在C#并发编程里,Span和Memory是两种轻量且高效的内存抽象类型,能够显著降低内存分配与拷贝开销,从而提升多线程程序的吞吐量和响应速度。理解它们的工作方式,对编写高性能服务端代码非常有帮助。

为什么并发性能会被内存操作拖慢
传统做法中,处理字节流或数组片段时常会创建新数组或返回副本,这会导致:
- 频繁堆分配,增加GC负担,引发停顿
- 多线程下大量对象分配加剧锁竞争
- 缓存局部性差,CPU等待内存延迟变长
Span<T>和Memory<T>让我们可以在不复制数据的前提下,安全地引用一段连续内存。
Span与Memory的基本区别
Span<T>是仅存在于栈上的ref struct,不能用于异步方法或跨线程捕获;Memory<T>是可存放在堆上的结构体,能够安全地传递给Task或在线程间共享。
| 类型 | 存储位置 | 能否跨线程/异步 |
|---|---|---|
| Span<T> | 栈 | 否 |
| Memory<T> | 堆或栈引用 | 是 |
用Memory减少并发中的数据复制
在生产者消费者模型中,使用Memory<T>传递缓冲区,可避免每轮都分配新数组:
using System;
using System.Threading.Tasks;
class BufferPool
{
private readonly Memory<byte> _shared = new byte[1024];
public async Task ProcessAsync()
{
// 将同一块内存切片交给不同任务,无额外分配
Memory<byte> slice = _shared.Slice(0, 512);
await Task.Run(() => Handle(slice));
}
private void Handle(Memory<byte> data)
{
// 在独立线程中安全读写,不会引发堆复制
data.Span[0] = 1;
}
}
用Span在热路径中避免GC
对于同步计算密集代码,Span可配合stackalloc在栈上分配,彻底绕开GC:
using System;
class Parser
{
public int Sum(ReadOnlySpan<byte> input)
{
int total = 0;
for (int i = 0; i < input.Length; i++)
{
total += input[i];
}
return total;
}
public void Run()
{
// 栈上分配,无堆压力
Span<byte> local = stackalloc byte[256];
local[0] = 10;
Console.WriteLine(Sum(local));
}
}
并发场景下的实践建议
减少锁内拷贝
锁保护区域内尽量只传递Span或Memory,而不是复制数组,缩短临界区时间。
结合ArrayPool
使用ArrayPool<T>.Shared租借数组,再包装为Memory对外暴露,进一步降低分配:
using System;
using System.Buffers;
class Worker
{
public void Do()
{
byte[] buf = ArrayPool<byte>.Shared.Rent(1024);
try
{
Memory<byte> mem = buf;
// 多线程读取mem,无需每次new byte[1024]
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
}
小结
Span和Memory通过栈上访问、零复制共享和可控的生命周期,帮助C#应用在并发环境中减少GC与锁竞争。同步热路径优先用Span,异步或跨线程传递则用Memory,再配合ArrayPool即可获得明显的性能提升。