C#中的ArrayPool、MemoryPool
在C#中,ArrayPool<T>和MemoryPool<T>都是用来提高性能、减少内存分配和垃圾回收(GC)压力的工具。
它们通过重用已经分配的内存块来实现这一点,而不是每次都分配新的内存。
1. ArrayPool
ArrayPool<T> 是一个用于租用和归还数组的池。它主要用于需要频繁创建和销毁数组的场景,以减少GC的压力。
本质:
ArrayPool<T>是一个静态类,提供了共享的数组池实例(通过ArrayPool<T>.Shared访问)。- 它内部维护了一系列不同大小的数组(通常是2的幂),当你请求一个数组时,它会尝试从池中找到一个大小合适的数组(可能大于或等于你请求的大小),如果没有找到,它会分配一个新的数组。
- 当你使用完数组后,你应该将其归还到池中,以便后续重用。
使用场景:
- 当你需要临时使用一个大数组,并且这个数组的生命周期很短,频繁创建和销毁会导致GC压力增大时。
- 例如:网络编程中处理数据包,文件读写缓冲区等。
示例代码:
using System.Buffers;
// 租用一个最小长度为1024的数组
var arrayPool = ArrayPool<byte>.Shared;
byte[] buffer = arrayPool.Rent(1024);
try
{
// 使用buffer
// 注意:Rent返回的数组长度可能大于1024,所以实际使用时要根据需求使用,不要超过你需要的长度
// 例如,如果你只需要1024个字节,那么就不要使用超过buffer[0..1024]
}
finally
{
// 使用完后归还
arrayPool.Return(buffer);
}
2. MemoryPool
MemoryPool<T> 是一个用于租用IMemoryOwner<T>(它包装了Memory<T>)的池。Memory<T>是Span相关类型的一部分,用于表示一块连续的内存,且比ArrayPool<T>更灵活,因为它可以表示不仅仅是数组,还可以是其他内存(如非托管内存)。
本质:
MemoryPool<T>是一个抽象类,通常使用其默认实现ArrayMemoryPool<T>(内部使用ArrayPool<T>)或者自定义实现。- 通过
MemoryPool<T>.Shared可以获取一个基于数组的共享内存池。 - 它返回的是一个
IMemoryOwner<T>,这个接口实现了IDisposable,当调用Dispose时,内存会被自动归还到池中。
使用场景:
- 当你需要一块连续的内存,并且希望以
Memory<T>的形式来操作,同时希望自动管理内存的归还(通过using语句)。 - 在异步编程中,因为
Memory<T>可以跨越异步方法,而Span<T>不能(因为它是一个ref struct)。
示例代码:
using System.Buffers;
var memoryPool = MemoryPool<byte>.Shared;
using (IMemoryOwner<byte> owner = memoryPool.Rent(1024))
{
Memory<byte> memory = owner.Memory;
// 使用memory,例如写入数据
// 同样,实际分配的内存可能大于请求的大小,所以使用时要根据请求的长度来操作,不要超过你需要的部分。
// 例如:memory.Slice(0, 1024)
}
// 当离开using块时,owner.Dispose()会被调用,自动归还内存到池中
两者的主要区别
ArrayPool<T>直接操作数组(T[]),而MemoryPool<T>操作的是IMemoryOwner<T>(它包含一个Memory<T>)。MemoryPool<T>在默认实现中内部使用了ArrayPool<T>,但它提供了更高级别的抽象,特别是对Memory<T>的支持,这使得它在异步和更广泛的内存管理场景中更有用。MemoryPool<T>通过IMemoryOwner<T>的Dispose自动归还内存,而ArrayPool<T>需要显式调用Return方法。
总结
两者都是为了减少内存分配和GC压力而设计的内存池机制。
ArrayPool<T>更底层,直接管理数组;
MemoryPool<T>则提供了更高级别的抽象,特别是与Memory<T>集成,并支持自动归还。
在选择时,如果你需要操作数组,并且可以手动管理归还,使用ArrayPool<T>;如果你需要Memory<T>,或者希望自动归还,使用MemoryPool<T>。
在 C# 中,ArrayPool<T> 和 MemoryPool<T> 是用于高性能内存管理的核心组件,它们的本质是通过重用内存缓冲区来减少 GC 压力和提升性能。以下是详细说明:
1. ArrayPool<T>
作用:
- 重用数组:避免频繁分配/释放
T[]数组,减少垃圾回收(GC)开销 - 减少内存碎片:适用于需要临时缓冲区的场景(如序列化、网络 I/O)
- 典型场景:处理文件/网络数据时的临时缓冲区
本质:
- 内部维护一个数组的共享池(线程安全)
- 租用(Rent)时返回一个长度可能大于请求大小的数组(策略优化)
- 归还(Return)时标记为可重用,不清除数据(需手动处理敏感数据)
使用示例:
using System.Buffers;
// 租用最小长度为 1024 的数组
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(minimumLength: 1024);
try
{
// 使用缓冲区(实际长度可能 >1024)
int bytesRead = ReadData(buffer);
ProcessData(buffer.AsSpan(0, bytesRead));
}
finally
{
// 归还数组
pool.Return(buffer);
}
2. MemoryPool<T>
作用:
- 分配可重用的
Memory<T>或IMemoryOwner<T>对象 - 支持更广泛的内存来源(不仅限于数组,还包含非托管内存等)
- 与
System.IO.Pipelines等高级 API 深度集成
本质:
- 返回一个带生命周期的内存包装器 (
IMemoryOwner<T>) - 通过
Dispose()自动归还内存(RAII 模式) - 默认实现基于
ArrayPool<T>,但可扩展
使用示例:
using System.Buffers;
// 租用内存
var pool = MemoryPool<byte>.Shared;
using (IMemoryOwner<byte> owner = pool.Rent(minimumLength: 1024))
{
Memory<byte> memory = owner.Memory;
// 安全使用内存
ReadData(memory.Span);
ProcessData(memory.Slice(0, 100));
} // 此处自动调用 Dispose() 归还内存
关键对比
| 特性 | ArrayPool<T> |
MemoryPool<T> |
|---|---|---|
| 返回类型 | T[] |
IMemoryOwner<T>(包装 Memory<T>) |
| 归还方式 | 显式调用 Return() |
通过 Dispose() 自动归还 |
| 内存来源 | 仅托管数组 | 可扩展(数组、非托管内存等) |
| 线程安全 | 是 | 是 |
| 典型使用场景 | 直接操作数组的底层场景 | 高级 API(如 Pipelines)或需要自动生命周期管理 |
设计本质总结
-
对象池模式
两者都是内存缓冲区的对象池,避免重复分配/回收。 -
性能优化
通过重用缓冲区:- 减少 GC 触发频率
- 降低内存分配开销
- 避免 LOH(大对象堆)碎片
-
作用域差异
ArrayPool<T>:面向数组的直接操作MemoryPool<T>:面向更抽象的内存块(支持Span<T>/Memory<T>生态)
何时使用?
- 需要短期使用大型缓冲区时(如处理 1KB+ 数据)
- 在高性能场景中替代
new T[] - 在库开发中减少对调用方造成 GC 压力
⚠️ 注意:归还缓冲区后不要继续使用其引用,因为内容可能被后续租用者修改。敏感数据需在归还前手动清除。

浙公网安备 33010602011771号