ConcurrentNativeQueue这个名字听起来像是一个并发队列的实现,可能是用于Java或其他支持并发编程的语言。以下是一些关于如何实现或使用这样一个队列的基本概念:### 定义ConcurrentNativeQueue 是一个线程安全的队列,它
摘要:ConcurrentNativeQueue<T>:一个使用 .NET 实现的零 GC 压力的无锁 MPSC 原生队列 一、为什么要造这个轮子 .NET 提供了 ConcurrentQueue&am
ConcurrentNativeQueue<T>:一个使用 .NET 实现的零 GC 压力的无锁 MPSC 原生队列
一、为什么要造这个轮子
.NET 提供了 ConcurrentQueue<T> 和 Channel<T> 两种开箱即用的并发队列。对大多数业务场景,它们已经足够好。但在以下场景中,它们的底层设计决策会成为性能瓶颈:
游戏主循环 / 音频管线:GC 停顿(即使是 Gen0)会导致可感知的帧卡顿或音频爆音。即使 Workstation GC 的 Gen0 暂停只有 ~100μs,在 16ms 的帧预算中也可能造成掉帧。
高频交易 / 实时数据管线:每微秒都有价值,托管堆分配意味着不可预测的 GC 介入。
Native interop 密集场景:数据需要频繁在 managed/unmanaged 边界传递,如果队列本身就在 native 内存上,可以省去 pin/copy 开销。
AOT 发布:ConcurrentNativeQueue<T> 是纯 unmanaged 结构体,天然适合 NativeAOT 场景。
ConcurrentNativeQueue<T> 的目标很明确:在 MPSC(多生产者单消费者)模式下,提供零 GC 压力、零托管堆分配的高吞吐量队列。这不是要替代 ConcurrentQueue<T>,而是为那些"对 GC 停顿零容忍"的场景提供一个专用工具。
二、整体架构
ConcurrentNativeQueue<T> (struct, unmanaged)
├── _head ──→ [SegmentHeader*] ──→ [SegmentHeader*] ──→ ...
│ ↓ ↓
│ [Slot* 数组] [Slot* 数组]
│ (已消费,释放) (消费中)
│
├── _tail ──→ [SegmentHeader*] ──→ (预建的下一段)
│ ↓
│ [Slot* 数组]
│ (生产中)
│
├── _origin ──→ 首段(用于 Dispose 遍历释放所有段头)
│
└── 缓存行填充:_head/_dequeuePos 与 _tail 之间 64 字节隔离
所有内存(段头结构体 + 槽位数组)均通过 NativeMemory 分配。ConcurrentNativeQueue<T> 本身也是 struct,整个生命周期不产生任何托管堆分配。
三、核心技术原理
3.1 无锁入队:Volatile.Read + CAS
入队操作不使用锁,核心路径只有一次原子操作:
// 1. 纯读检测:当前段是否有空位
long pos = Volatile.Read(ref tail->EnqueuePos.Value);
long offset = pos - tail->BaseIndex;
if (offset >= tail->Capacity) { /* 段满,推进到下一段 */ }
// 2. CAS 占位
if (Interlocked.CompareExchange(ref tail->EnqueuePos.Value, pos + 1, pos) == pos)
{
// 3. 写入数据,设置标记
tail->Slots[offset].Value = item;
Volatile.Write(ref tail->Slots[offset].State, 1);
}
关键设计点:
段满检测是纯读操作(Volatile.Read),不产生任何原子写。多个生产者同时检测到段满时,只有 read 竞争,不会弄脏 cache line。
CAS 只在段有空位时才执行。段满时 Volatile.Read 发现 offset >= Capacity 后直接走段切换路径,避免了无效的原子写。
每次入队只有 1 次原子操作(CAS)。
