ConcurrentQueue 并非纯无锁,其在扩容 segment 和首次初始化 head/tail 时使用 Interlocked.CompareExchange 配合自旋回退,属 lock-free 但非 wait-free;核心通过分段设计与原子指令避免锁竞争,不使用 Monitor.Enter 或 lock。

为什么 ConcurrentQueue 不是纯无锁但表现接近
很多人以为 ConcurrentQueue 是完全 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 的 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 替代手写无锁队列
如果你要解决的是「高吞吐异步生产消费」,而不是「教学或性能极限压测」,Channel 是更现实的选择。它底层基于 ConcurrentQueue,但封装了背压、取消、完成状态等,且 WriteAsync/ReadAsync 在无竞争时几乎零分配。
- 同步场景用
ConcurrentQueue,足够快也足够稳 - 需要限流或取消支持,直接用
Channel.CreateBounded(size) - 别碰
SpinWait+Unsafe手写——除非你在写System.Threading.Channels本身
真正难的从来不是原子操作本身,而是定义清楚「什么状态算一致」以及「谁负责清理中间态」。这两个问题没想透之前,所有 Interlocked 调用都只是给崩溃加了随机延迟。










