---
id: bytepool
title: 内存池
---

### 定义

命名空间：TouchSocket.Core <br/>
程序集：[TouchSocket.Core.dll](https://www.nuget.org/packages/TouchSocket.Core)


## 一、说明

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

## 二、功能

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

内存池的最小实现单体是`内存块（ByteBlock）`和`值内存块（ValueByteBlock）`。`ByteBlock`是继承自`Stream`的类，拥有和`MemoryStream`一样的功能和内存回收管理的增强功能。所以如果有`MemoryStream`的使用需求的话，就可以完全让`ByteBlock`替代。而`ValueByteBlock`是`ByteBlock`的值类型（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，如果均已派出，则直接新建。

```csharp showLineNumbers
BytePool bytePool = new BytePool(maxArrayLength: 1024 * 1024, maxArraysPerBucket: 50)
{
    AutoZero = false,//在回收内存时，是否清空内存
    MaxBucketsToTry = 5//最大梯度跨度
};

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

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

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

使用内存块

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

- byteSize：用于申请的最小字节尺寸。例如：当申请100长度时，可能会得到100，1000，甚至更大尺寸的内存，但绝不会小于100.

```csharp showLineNumbers
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创建及释放时，均在默认内存池
{
}
```

:::caution 注意

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

:::  

:::danger 危险

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

:::  

### 3.2 基本数组的写入和读取

基本使用和`MemoryStream`一致。

```csharp showLineNumbers
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 基础类型的写入和读取

```csharp showLineNumbers
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();
}
```

:::caution 注意

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

:::  

### 3.4 数据对象的写入的读取

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

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

    byteBlock.SeekToStart();

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

### 3.5 数组包的写入和读取

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

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

    byteBlock.SeekToStart();

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

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

```csharp showLineNumbers
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 包类型的写入和读取

[包类型](./ipackage.mdx)是最高效的数据结构转换方案。所以内存池也支持按序排入。

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

    byteBlock.SeekToStart();

    var myPackage = byteBlock.ReadPackage<MyPackage>();
}
```

```csharp showLineNumbers
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中获取数据，所以，必定发生异常。**

```csharp showLineNumbers
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。

```csharp showLineNumbers
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;
    }
}
```

:::caution 注意

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

:::  


### 3.8 其他用法

:::tip 提示

**ByteBlock（ValueByteBlock）**的实际数据有效长度应该是`ByteBlock.Length`属性，而不是`ByteBlock.Buffer.Length`。

:::  

:::tip 提示

由于**ByteBlock（ValueByteBlock）**的部分属性是long类型，但是在使用时有时候需要int类型，所以也提供了int的封装转换，例如：`ByteBlock.Length`和`ByteBlock.Len`。

:::  


[本文示例Demo](https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/Core/BytePoolConsoleApp)