c# Span 和 Memory 如何提升并发性能

来源:这里教程网 时间:2026-02-21 17:38:45 作者:

Span 和 Memory 本身不提升并发性能

这是最关键的误判点:

Span<t></t>
Memory<t></t>
不是并发原语,它们不带锁、不保证线程安全、也不参与调度。用错场景反而引发
System.IndexOutOfRangeException
ObjectDisposedException
(尤其在跨线程传递
Span<t></t>
时)。

它们的价值在于:避免堆分配、减少 GC 压力、绕过数组边界检查(JIT 优化后),从而间接支撑高并发吞吐——前提是你的并发瓶颈真正在内存分配或拷贝上。

哪些并发场景能真正受益

典型有效场景集中在「短生命周期、高频率、只读或单线程写+多线程读」的数据处理链路中:

HTTP 请求体解析(如 JSON body 转
Span<byte></byte>
后直接切片解析,不 new byte[])
Socket 接收缓冲区复用(
Memory<byte></byte>
持有池化
ArrayPool<byte>.Shared</byte>
分配的数组,避免每次接收都 new)
高性能日志格式化(用
Span<char></char>
拼接结构化字段,配合
IBufferWriter<char></char>
直写到输出流)
无锁队列中传递只读数据快照(
Memory<t></t>
可跨线程传递;
Span<t></t>
绝对不可)

Memory 是并发友好的唯一选择

Span<t></t>
是 ref-like 类型,绑定栈帧或特定对象生命周期,**不能作为字段、不能存储在堆对象中、不能跨 await 边界、更不能在线程间传递**。试图这么做会触发编译错误或运行时
System.ArgumentException: Span<t> cannot be used in this context</t>

Memory<t></t>
才是为异步/并发设计的轻量包装器,它可安全持有并传递,但需注意:

底层仍可能指向堆数组(如
new byte[1024]
)、栈内存(
stackalloc
,但无法跨方法返回)或本机内存(
NativeMemory.Allocate
若用
ArrayPool<byte>.Shared.Rent()</byte>
创建
Memory<byte></byte>
,必须确保归还(
Return()
),否则池耗尽会导致新分配退化为
new byte[]
多个线程同时读同一
Memory<t></t>
安全;但若某线程在写底层数组(比如通过
Memory.Span
修改),其他线程读就构成竞态——这和普通数组一样,需额外同步

真实代码中怎么写才不翻车

以下是一个 Socket 接收 + 复用缓冲区的典型模式,体现

Memory<byte></byte>
如何降低分配压力:

private static readonly ArrayPool<byte> _pool = ArrayPool<byte>.Shared;
public async Task ProcessRequestAsync(Socket socket)
{
    var buffer = _pool.Rent(8192);
    try
    {
        var memory = new Memory<byte>(buffer);
        int bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None);
        
        // ✅ 安全:只读切片,不修改 buffer 内容
        var payload = memory.Slice(0, bytesRead);
        ParseRequest(payload); // 接收端逻辑,入参为 ReadOnlySpan<byte>
    }
    finally
    {
        // ✅ 必须归还,否则池泄漏
        _pool.Return(buffer);
    }
}

注意三点:

不要把
memory.Slice(...)
存成类字段或传给其他线程长期持有——
Memory<t></t>
不管理生命周期,只依赖你正确归还底层数组
不要在
ParseRequest
内部保存
ReadOnlySpan<byte></byte>
到字段——它会随栈帧销毁而失效
如果解析过程需要异步等待(比如查数据库),必须先将关键数据拷贝出来(
ToArray()
或写入目标对象),不能依赖原始
Span
/
Memory

真正卡并发性能的,往往不是 Span 或 Memory 用得够不够“炫”,而是有没有把它们嵌进正确的内存生命周期里——池没配对、切片越界、跨线程误传

Span
,比不用它们还容易出问题。

相关推荐