跳到主要内容
版本:2.0.0

内存池

定义

命名空间:TouchSocket.Core
程序集:TouchSocket.Core.dll

一、说明

内存池是TouchSocket系列的最重要的组成部分,在TouchSocket产品中,BytePool贯穿始终。所以熟练使用BytePool,也是非常重要的。

二、功能

内存池(BytePool)是解决创建大量字节数组消耗的最有力手段,其实质完全借鉴了微软的ArrayPool。而且在此基础上做了更多的优化。

内存池的最小实现单体是内存块(ByteBlock)值内存块(ValueByteBlock)ByteBlock是继承自Stream的类,拥有和MemoryStream一样的功能和内存回收管理的增强功能。所以如果有MemoryStream的使用需求的话,就可以完全让ByteBlock替代。而ValueByteBlockByteBlock的值类型(ref struct),其功能除了不继承Stream以外,和ByteBlock一模一样。且为值类型,创建开销更小。

三、使用

3.1 创建、回收

BytePool在默认情况提供了一个Default的默认实例,当然您可以新建只属于自己的BytePool

其中:

  • maxArrayLength,是内存池的最大字节数组尺寸。
  • maxArraysPerBucket是每个内存块桶的最大数组数量。
  • AutoZero属性,在回收内存时,是否清空内存。
  • MaxBucketsToTry是最大梯度跨度。例如:当梯度为16、32、64、128、512、1024时,MaxBucketsToTry为2,则当请求的长度是16时,且16的内存块均已派出,则会请求32,最大会请求到64,如果均已派出,则直接新建。
BytePool bytePool = new BytePool(maxArrayLength: 1024 * 1024, maxArraysPerBucket: 50)
{
AutoZero = false,//在回收内存时,是否清空内存
MaxBucketsToTry = 5//最大梯度跨度
};

BytePool defaultBytePool = BytePool.Default;//使用默认的

在内存池创建以后,可以直观的查看它的各个属性。包括:

Console.WriteLine($"内存池容量={bytePool.Capacity}");
Console.WriteLine($"内存池实际尺寸={bytePool.GetPoolSize()}");

使用内存块

ByteBlock可通过BytePool实例创建,也可以直接new对象,后者使用的是默认内存池实例提供支持。

  • byteSize:用于申请的最小字节尺寸。例如:当申请100长度时,可能会得到100,1000,甚至更大尺寸的内存,但绝不会小于100.
ByteBlock byteBlock1 = new ByteBlock(byteSize: 1024 * 1024);//从默认内存池获得
byteBlock1.Dispose();

BytePool bytePool = new BytePool();
ByteBlock byteBlock2 = bytePool.GetByteBlock(byteSize: 1024 * 1024);//从指定内存池获得

using (ByteBlock byteBlock3 = new ByteBlock())//通过using创建及释放时,均在默认内存池
{
}
注意

创建的**ByteBlock(ValueByteBlock)**必须显示释放(Dispose),可用using,如果不释放,虽然不会内存泄露,但是会影响性能。

危险

无论何时何地,都不要直接引用ByteBlock.Buffer,可以直接使用。如果需要引用实际数据,请使用Read、ToArray等方法可复制导出新的数据内存。

3.2 基本数组的写入和读取

基本使用和MemoryStream一致。

using (var byteBlock = new ByteBlock())
{
byteBlock.Write(new byte[] { 0, 1, 2, 3 });//将字节数组写入

byteBlock.SeekToStart();//将游标重置

var buffer = new byte[byteBlock.Len];//定义一个数组容器
var r = byteBlock.Read(buffer);//读取数据到容器,并返回读取的长度r
}

3.3 基础类型的写入和读取

using (var byteBlock = new ByteBlock())
{
byteBlock.Write(byte.MaxValue);//写入byte类型
byteBlock.Write(int.MaxValue);//写入int类型
byteBlock.Write(long.MaxValue);//写入long类型
byteBlock.Write("RRQM");//写入字符串类型

byteBlock.SeekToStart();//读取时,先将游标移动到初始写入的位置,然后按写入顺序,依次读取

byte byteValue = (byte)byteBlock.ReadByte();
int intValue = byteBlock.ReadInt32();
long longValue = byteBlock.ReadInt64();
string stringValue = byteBlock.ReadString();
}
注意

在写入字符串时,如果想要按序写入并读取,应该使用byteBlock.Write,而非byteBlock.WriteString。因为byteBlock.WriteString是直接把字符串转为utf-8编码,然后写入。

3.4 数据对象的写入的读取

数据对象的写入和读取,需要指定序列化类型。因为他的实质是序列化和反序列化。

using (var byteBlock = new ByteBlock())
{
//将实例写入,实际上是序列化
byteBlock.WriteObject(new MyClass(), SerializationType.FastBinary);

byteBlock.SeekToStart();

//读取实例,实际上是反序列化
var myClass = byteBlock.ReadObject<MyClass>();
}

3.5 数组包的写入和读取

数组包的写入和读取,也是可以按序排入的。

using (var byteBlock = new ByteBlock())
{
byteBlock.WriteBytesPackage(Encoding.UTF8.GetBytes("TouchSocket"));

byteBlock.SeekToStart();

byte[] bytes = byteBlock.ReadBytesPackage();
}

不过,在性能极致要求的情况下,使用ReadBytesPackage,实际上会创建额外的内存,所以可以使用下面用法:

using (var byteBlock = new ByteBlock())
{
byteBlock.WriteBytesPackage(Encoding.UTF8.GetBytes("TouchSocket"));

byteBlock.SeekToStart();

//使用下列方式即可高效完成读取
if (byteBlock.TryReadBytesPackageInfo(out int pos,out int len))
{
var str=Encoding.UTF8.GetString(byteBlock.Buffer,pos,len);
}
}

3.6 包类型的写入和读取

包类型是最高效的数据结构转换方案。所以内存池也支持按序排入。

using (var byteBlock = new ByteBlock())
{
byteBlock.WritePackage(new MyPackage()
{
Property = 10
});

byteBlock.SeekToStart();

var myPackage = byteBlock.ReadPackage<MyPackage>();
}
class MyPackage : PackageBase
{
public int Property { get; set; }

public override void Package(in ByteBlock byteBlock)
{
byteBlock.Write(this.Property);
}

public override void Unpackage(in ByteBlock byteBlock)
{
this.Property=byteBlock.ReadInt32();
}
}

3.7 多线程同步协作(Hold)

在多线程异步时,设计架构应当遵守谁(Thread)创建的ByteBlock,由谁释放,这样就能很好的避免未释放的情况发生。实际上TouchSocket中,就是秉承这样的设计,任何非用户创建的ByteBlock,都会由创建的线程最后释放。但是在使用中,经常出现异步多线程的操作。

以TouchSocket的TcpClient为例。如果直接在收到数据时,使用Task异步,则必定会发生关于ByteBlock的各种各样的异常。

原因非常简单,byteBlock对象在到达HandleReceivedData时,触发Task异步,此时触发线程会立即返回,并释放byteBlock,而Task异步线程会滞后,然后试图从已释放的byteBlock中获取数据,所以,必定发生异常。

public class MyTClient : TcpClient
{
protected override bool HandleReceivedData(ByteBlock byteBlock, IRequestInfo requestInfo)
{
Task.Run(()=>
{
string mes = Encoding.UTF8.GetString(byteBlock.Buffer, 0, byteBlock.Len);
Console.WriteLine($"已接收到信息:{mes}");
});
return true;
}
}

解决方法也非常简单,只需要在异步前锁定,然后使用完成后取消锁定,且不用再调用Dispose。

public class MyTClient : TcpClient
{
protected override bool HandleReceivedData(ByteBlock byteBlock, IRequestInfo requestInfo)
{
byteBlock.SetHolding(true);//异步前锁定
Task.Run(()=>
{
string mes = Encoding.UTF8.GetString(byteBlock.Buffer, 0, byteBlock.Len);
byteBlock.SetHolding(false);//使用完成后取消锁定,且不用再调用Dispose
Console.WriteLine($"已接收到信息:{mes}");
});
return true;
}
}
注意

ByteBlock在设置SetHolding(false)后,不需要再调用Dispose。

3.8 其他用法

提示

**ByteBlock(ValueByteBlock)**的实际数据有效长度应该是ByteBlock.Length属性,而不是ByteBlock.Buffer.Length

提示

由于**ByteBlock(ValueByteBlock)**的部分属性是long类型,但是在使用时有时候需要int类型,所以也提供了int的封装转换,例如:ByteBlock.LengthByteBlock.Len

本文示例Demo