c# 无锁队列的实现原理 c# lock-free 数据结构

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

为什么
ConcurrentQueue<t></t>
不是纯无锁但表现接近

很多人以为

ConcurrentQueue<t></t>
是完全 lock-free 的,其实它内部在**扩容 segment** 和**首次初始化 head/tail 节点**时仍会用到
Interlocked.CompareExchange
配合少量自旋+回退逻辑,严格来说属于「无锁(lock-free)但非等待无关(wait-free)」。它的核心设计是把竞争拆到多个独立 segment 上,让绝大多数入队/出队操作只依赖
Interlocked
原子指令完成,避免了
lock
语句带来的线程挂起开销。

关键点在于:它不使用

Monitor.Enter
lock(obj)
,所有共享状态更新都靠
Interlocked.Increment
Interlocked.CompareExchange
等实现——这是 lock-free 的底线。

ConcurrentQueue<t></t>
的 head/tail 分离与 ABA 问题规避

它用两个独立的 volatile 字段

_head
_tail
分别指向当前可消费/可插入的节点,避免单指针更新时的 ABA 冲突。每次
Enqueue
尝试用
Interlocked.CompareExchange
更新
_tail
,失败就重试;
Dequeue
同理操作
_head
。但真正巧妙的是:它不直接修改节点的
Next
引用,而是先用
Interlocked.CompareExchange
把新节点挂到当前 tail 的
Next
,再尝试推进
_tail
—— 这样即使发生 ABA,也不会破坏链表结构。

节点
Next
字段声明为
volatile
,确保可见性
每个 segment 固定大小(默认 32),满后原子切换到新 segment,避免长链表遍历 没有使用
Unsafe
或指针,纯托管代码,兼容 GC 和跨平台运行时

自己写 lock-free 队列前必须面对的三个硬伤

手写生产级 lock-free 队列远比看起来危险。.NET 的内存模型、JIT 重排序、GC 移动对象都会悄悄破坏你的假设:

volatile
不能阻止所有重排序,某些场景需搭配
Thread.MemoryBarrier
Interlocked
指令
节点对象被 GC 回收后,其他线程可能还在读它的字段(dangling reference),
ConcurrentQueue
用「节点永不删除」策略规避——你很难安全复现
ABA 问题在 .NET 中更隐蔽:不是整数被改回原值,而是引用被回收又分配到同一地址(尤其在 Server GC 下)

例如下面这段看似正确的入队逻辑:

var currentTail = _tail;
var newNext = currentTail.Next;
if (newNext == null && Interlocked.CompareExchange(ref currentTail.Next, newNode, null) == null)
{
    Interlocked.CompareExchange(ref _tail, newNode, currentTail);
}

实际会因

currentTail
是局部副本而失效——你必须用
Interlocked.CompareExchange(ref _tail, ...)
得到最新值,否则永远在过期节点上操作。

什么时候该用
Channel<t></t>
替代手写无锁队列

如果你要解决的是「高吞吐异步生产消费」,而不是「教学或性能极限压测」,

Channel<t></t>
是更现实的选择。它底层基于
ConcurrentQueue<t></t>
,但封装了背压、取消、完成状态等,且
WriteAsync
/
ReadAsync
在无竞争时几乎零分配。

同步场景用
ConcurrentQueue<t></t>
,足够快也足够稳
需要限流或取消支持,直接用
Channel.CreateBounded<t>(size)</t>
别碰
SpinWait
+
Unsafe
手写——除非你在写
System.Threading.Channels
本身

真正难的从来不是原子操作本身,而是定义清楚「什么状态算一致」以及「谁负责清理中间态」。这两个问题没想透之前,所有

Interlocked
调用都只是给崩溃加了随机延迟。

相关推荐