数据结构 --- 无锁队列(Lock-Free Queue)

在高并发系统中,队列是线程间数据传递、任务调度和异步处理的基础组件。传统线程安全队列通常通过 std::mutex 保护共享状态,实现简单且正确性易于保障,但在高竞争场景下,锁竞争会导致线程阻塞、调度延迟与上下文切换开销,显著影响系统实时性与吞吐能力。

无锁队列(Lock-Free Queue)利用原子操作(Atomic Operation)、CAS(Compare-And-Swap)原语以及 C++ 内存模型,在不依赖传统互斥锁保护核心数据结构的前提下,实现多线程之间的安全并发协作。

无锁不等于没有等待,也不意味着性能一定优于有锁队列。真正理解无锁队列,需要同时掌握并发进展保证、线性化语义、内存序约束、ABA 问题与安全内存回收等知识体系。

一、无锁队列

1.1 为什么需要无锁队列?

传统线程安全队列一般通过互斥锁保护数据结构的并发访问:

cpp 复制代码
std::queue<int> queue;
std::mutex mtx;

void enqueue(int value) {
    std::lock_guard<std::mutex> lock(mtx);
    queue.push(value);
}

bool dequeue(int& value) {
    std::lock_guard<std::mutex> lock(mtx);

    if (queue.empty()) {
        return false;
    }

    value = queue.front();
    queue.pop();
    return true;
}

这种方式能够保证多个线程不会同时修改队列内部状态,但存在明确的性能成本:当线程 A 持有互斥锁时,线程 B 若尝试访问队列,必须等待线程 A 释放锁。根据互斥锁实现与竞争程度,等待可能表现为自旋等待、线程阻塞或操作系统重新调度。

在高频数据采集、实时控制、高并发任务调度等延迟敏感场景中,这些等待会显著增加系统延迟及其波动。无锁队列则通过原子操作实现并发状态更新,使线程无需获取传统互斥锁即可尝试入队或出队操作。

有锁队列的性能瓶颈来自具体的锁竞争和临界区开销,而非 std::mutex 本身必然低效。如果队列并发度较低、任务处理时间较长,有锁队列完全可能达到更优的综合性能。

1.2 Lock-Free 与 Wait-Free 的区别

无锁编程中的"无锁"实际上包含多种不同层级的并发进展保证(Progress Guarantee):

类型 进展保证 特点
Blocking 非阻塞进展保证 线程可能等待其他线程释放锁
Obstruction-Free 单线程独占执行时能够完成 存在多线程竞争时可能持续失败
Lock-Free 系统整体持续取得进展 全局保证推进,单个线程可能不断重试CAS失败
Wait-Free 每个操作都能在有界步骤内完成 不依赖其他线程的执行速度,所有线程都能在有限步内完成操作

其中最核心的是 Lock-Free 与 Wait-Free 两类:

Lock-Free(无锁) 保证在持续执行操作的线程集合中,系统整体能够持续取得进展。即使某个线程被暂停,其他线程仍然有机会完成操作;但它不保证每个线程都能在有限步骤内成功。

典型的 Lock-Free 操作模式:

cpp 复制代码
while (!CAS(...)) {
    // CAS失败,重新读取状态并重试
}

如果多个线程持续竞争同一个原子变量,其中一个线程可能反复 CAS 失败。只要系统整体仍有操作成功完成,就满足 Lock-Free 的进展保证。

Wait-Free(无等待) 则提供更强的保证:每个操作都能在有界步骤内完成,完全不依赖其他线程的执行速度与调度状态。

此外需要区分一个易混淆概念:Non-Blocking API。例如 try_push / try_pop 这类在队列满或空时立即返回的接口,仅代表接口语义不阻塞,并不意味着内部实现一定满足 Lock-Free 或 Wait-Free 进展保证。"调用不会等待数据"和"算法具有无锁进展保证",是两个完全不同层面的概念。

1.3 并发队列必须满足线性化

线程安全不仅意味着程序没有数据竞争,还需要保证队列操作在并发执行时符合 FIFO 语义,这里需要引入 Linearizability(线性化) 的概念。

线性化要求:每个成功的并发操作,都能够被视为在调用开始与返回之间的某个瞬间原子地生效,并且所有操作能够排列为一个符合队列规范的全局顺序,这个瞬间称为 Linearization Point(线性化点)。

对于队列而言,线性化能够回答三个问题:

  • 某个元素究竟在什么时候正式入队?
  • 两个消费者同时出队,如何保证不会取走同一个元素?
  • 两个生产者同时入队,如何定义它们的逻辑先后顺序?

因此,设计无锁队列需要分别证明两个性质:

  • Safety(安全性):不会发生错误的数据访问、重复消费或违反 FIFO 的状态变化。
  • Liveness(活性):算法能够在给定并发条件下持续取得进展。

二、无锁队列的并发模型与结构选择

2.1 SPSC、MPSC、SPMC 与 MPMC

无锁队列设计首先需要明确生产者与消费者的线程数量,不同并发模型的同步复杂度差异极大:

模型 生产者数量 消费者数量 典型应用场景
SPSC 单个 单个 数据采集线程 → 处理线程、单链路数据流
MPSC 多个 单个 多线程日志汇聚、多源数据单路汇总
SPMC 单个 多个 单生产者任务分发、配置广播
MPMC 多个 多个 通用线程池、任务队列、多对多数据交换

例如在 SPSC 场景中,只有生产者修改写入位置,只有消费者修改读取位置,因此不需要多个生产者通过 CAS 竞争同一个索引。而 MPMC 场景中,多个生产者同时申请写入位置、多个消费者同时竞争取出元素,需要复杂得多的原子状态更新机制。

不要因为 MPMC 功能最通用,就默认所有高并发场景都应该使用 MPMC。工程实践中应优先根据真实线程拓扑选择最简单的并发模型,避免引入不必要的复杂度。

2.2 环形缓冲区与链表队列

无锁队列常见的底层存储结构主要包括环形缓冲区与链表队列两类:

Ring Buffer(环形缓冲区)

预先分配固定容量的连续存储空间,通过读写索引控制生产与消费位置。优势是内存布局连续、缓存局部性好、操作过程中无需动态分配,容量上限明确;不足是需要处理队列满、背压以及槽位复用问题。

Linked Queue(链表队列)

通过节点和指针组织队列,使用原子操作连接节点或移动头尾指针。能够实现动态容量结构,但也带来了节点分配、指针生命周期、ABA 问题与安全内存回收等一系列工程难题。

两种结构的直观差异:

工程实践中,如果能够确定业务最大容量,通常值得优先选择有界环形队列方案。

业务流量上限已知 → 队列最大长度可以预先定死 → 优先选有界环形队列(固定容量 ring buffer),而不是无界队列

三、原子操作与 C++ 内存模型

3.1 CAS:并发状态更新的基础

CAS,即 Compare-And-Swap(比较并交换),是无锁算法最核心的硬件原语。其逻辑可以表示为:

cpp 复制代码
// 逻辑伪代码,并非线程安全实现
bool CAS(address, expected, desired) {
    if (*address == expected) {
        *address = desired;
        return true;
    }
    return false;
}

真实 CAS 必须由具备原子语义的硬件指令实现,不能通过普通的比较与赋值替代。CAS 的思想是:只有当共享变量仍然处于预期状态时,线程才允许提交状态更新。

C++11 标准提供了两个 CAS 原语:

  • compare_exchange_weak():允许出现 Spurious Failure(虚假失败),通常配合循环使用
  • compare_exchange_strong():不允许虚假失败,更适合不希望因虚假失败而重试的场景

使用示例:

cpp 复制代码
#include <atomic>

std::atomic<int> state{0};
int expected = 0;

bool success = state.compare_exchange_weak(
    expected,
    1,
    std::memory_order_acq_rel,
    std::memory_order_acquire
);

如果 state 当前值等于 expected,CAS 尝试将其更新为 1 并返回 true;否则操作失败,并将实际读取到的值写回 expected。

需要注意:两者不应简单理解为 Weak 必然更快,实际性能与处理器架构、编译器实现以及竞争程度密切相关。

3.2 原子性不等于内存有序性

在多线程环境中,即使共享变量使用了原子操作,也不意味着其他普通内存访问自动获得正确的同步关系。考虑经典的发布-消费场景:

cpp 复制代码
int data = 0;
std::atomic<bool> ready{false};

生产者线程:

cpp 复制代码
data = 100;
ready.store(true, std::memory_order_release);

消费者线程:

cpp 复制代码
if (ready.load(std::memory_order_acquire)) {
    int value = data; // 安全读取已发布的数据
}

如果消费者的 Acquire 读取到了生产者 Release 写入的 true,则这两个原子操作建立 synchronizes-with 关系,由此形成 happens-before 偏序:
#mermaid-svg-19Nux6htHWeKzsQW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-19Nux6htHWeKzsQW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-19Nux6htHWeKzsQW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-19Nux6htHWeKzsQW .error-icon{fill:#552222;}#mermaid-svg-19Nux6htHWeKzsQW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-19Nux6htHWeKzsQW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-19Nux6htHWeKzsQW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-19Nux6htHWeKzsQW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-19Nux6htHWeKzsQW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-19Nux6htHWeKzsQW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-19Nux6htHWeKzsQW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-19Nux6htHWeKzsQW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-19Nux6htHWeKzsQW .marker.cross{stroke:#333333;}#mermaid-svg-19Nux6htHWeKzsQW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-19Nux6htHWeKzsQW p{margin:0;}#mermaid-svg-19Nux6htHWeKzsQW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-19Nux6htHWeKzsQW .cluster-label text{fill:#333;}#mermaid-svg-19Nux6htHWeKzsQW .cluster-label span{color:#333;}#mermaid-svg-19Nux6htHWeKzsQW .cluster-label span p{background-color:transparent;}#mermaid-svg-19Nux6htHWeKzsQW .label text,#mermaid-svg-19Nux6htHWeKzsQW span{fill:#333;color:#333;}#mermaid-svg-19Nux6htHWeKzsQW .node rect,#mermaid-svg-19Nux6htHWeKzsQW .node circle,#mermaid-svg-19Nux6htHWeKzsQW .node ellipse,#mermaid-svg-19Nux6htHWeKzsQW .node polygon,#mermaid-svg-19Nux6htHWeKzsQW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-19Nux6htHWeKzsQW .rough-node .label text,#mermaid-svg-19Nux6htHWeKzsQW .node .label text,#mermaid-svg-19Nux6htHWeKzsQW .image-shape .label,#mermaid-svg-19Nux6htHWeKzsQW .icon-shape .label{text-anchor:middle;}#mermaid-svg-19Nux6htHWeKzsQW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-19Nux6htHWeKzsQW .rough-node .label,#mermaid-svg-19Nux6htHWeKzsQW .node .label,#mermaid-svg-19Nux6htHWeKzsQW .image-shape .label,#mermaid-svg-19Nux6htHWeKzsQW .icon-shape .label{text-align:center;}#mermaid-svg-19Nux6htHWeKzsQW .node.clickable{cursor:pointer;}#mermaid-svg-19Nux6htHWeKzsQW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-19Nux6htHWeKzsQW .arrowheadPath{fill:#333333;}#mermaid-svg-19Nux6htHWeKzsQW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-19Nux6htHWeKzsQW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-19Nux6htHWeKzsQW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-19Nux6htHWeKzsQW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-19Nux6htHWeKzsQW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-19Nux6htHWeKzsQW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-19Nux6htHWeKzsQW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-19Nux6htHWeKzsQW .cluster text{fill:#333;}#mermaid-svg-19Nux6htHWeKzsQW .cluster span{color:#333;}#mermaid-svg-19Nux6htHWeKzsQW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-19Nux6htHWeKzsQW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-19Nux6htHWeKzsQW rect.text{fill:none;stroke-width:0;}#mermaid-svg-19Nux6htHWeKzsQW .icon-shape,#mermaid-svg-19Nux6htHWeKzsQW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-19Nux6htHWeKzsQW .icon-shape p,#mermaid-svg-19Nux6htHWeKzsQW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-19Nux6htHWeKzsQW .icon-shape .label rect,#mermaid-svg-19Nux6htHWeKzsQW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-19Nux6htHWeKzsQW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-19Nux6htHWeKzsQW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-19Nux6htHWeKzsQW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-19Nux6htHWeKzsQW .prod>*{fill:#E3F2FD!important;stroke:#1E88E5!important;stroke-width:2px!important;color:#0D47A1!important;}#mermaid-svg-19Nux6htHWeKzsQW .prod span{fill:#E3F2FD!important;stroke:#1E88E5!important;stroke-width:2px!important;color:#0D47A1!important;}#mermaid-svg-19Nux6htHWeKzsQW .prod tspan{fill:#0D47A1!important;}#mermaid-svg-19Nux6htHWeKzsQW .release>*{fill:#FFF3E0!important;stroke:#FB8C00!important;stroke-width:2px!important;color:#E65100!important;}#mermaid-svg-19Nux6htHWeKzsQW .release span{fill:#FFF3E0!important;stroke:#FB8C00!important;stroke-width:2px!important;color:#E65100!important;}#mermaid-svg-19Nux6htHWeKzsQW .release tspan{fill:#E65100!important;}#mermaid-svg-19Nux6htHWeKzsQW .acquire>*{fill:#E0F2F1!important;stroke:#00897B!important;stroke-width:2px!important;color:#004D40!important;}#mermaid-svg-19Nux6htHWeKzsQW .acquire span{fill:#E0F2F1!important;stroke:#00897B!important;stroke-width:2px!important;color:#004D40!important;}#mermaid-svg-19Nux6htHWeKzsQW .acquire tspan{fill:#004D40!important;}#mermaid-svg-19Nux6htHWeKzsQW .read>*{fill:#F3E5F5!important;stroke:#8E24AA!important;stroke-width:2px!important;color:#4A148C!important;}#mermaid-svg-19Nux6htHWeKzsQW .read span{fill:#F3E5F5!important;stroke:#8E24AA!important;stroke-width:2px!important;color:#4A148C!important;}#mermaid-svg-19Nux6htHWeKzsQW .read tspan{fill:#4A148C!important;} Consumer
Producer
synchronizes-with
data = 100
ready.store(release)
ready.load(acquire)
读取 data

生产者在 Release 之前的所有写入,通过 happens-before 关系先于消费者 Acquire 之后的所有读取。因此消费者能够安全观察到对应的已发布数据。

这就是无锁队列的 发布---获取(Publish--Acquire)同步机制。

3.3 常用内存序

内存序 主要语义 常见用途
relaxed 仅保证原子性和单变量修改顺序,不额外建立同步 独立计数、线程私有索引读取
acquire 获取之前已发布的所有数据写入 消费者读取发布索引、读取共享状态
release 发布当前线程此前的所有内存写入 生产者发布写入完成、释放共享资源
acq_rel 同时具有获取与发布语义 CAS、fetch_add 等读改写操作
seq_cst 提供全局顺序一致性约束 简化并发推理、需要全局全序的场景

seq_cst 是原子操作的默认内存序,但并不意味着它在每种处理器和每个场景下都会显著降低性能。同样,relaxed 是不额外建立用于同步其他内存访问的 happens-before 关系。

四、SPSC 无锁队列的实现原理

相比通用 MPMC 队列,SPSC 更适合作为理解内存序的入门实现。因为 SPSC 只有一个生产者和一个消费者,可以在完全不使用 CAS 的情况下设计线程安全队列。

4.1 环形缓冲区的索引设计

假设缓冲区拥有 N 个物理槽位,使用两个原子索引:

cpp 复制代码
std::atomic<size_t> head; // 消费者下一次读取的位置
std::atomic<size_t> tail; // 生产者下一次写入的位置

索引推进采用环绕方式:

cpp 复制代码
next = (index + 1) % Capacity;

为了简单区分队空和队满,通常始终保留一个空槽位。此时:

  • 队空条件:head == tail
  • 队满条件:next(tail) == head

因此当物理槽位数量为 Capacity 时,实际可存储元素数为 Capacity - 1。该方案牺牲一个存储位置,但无需额外的满标志或序列号,极大简化了并发正确性分析。

4.2 为什么 SPSC 不需要 CAS?

SPSC 中,两个索引分别由不同线程独占修改权:
#mermaid-svg-s8dS99DZwE7uL07k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-s8dS99DZwE7uL07k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-s8dS99DZwE7uL07k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-s8dS99DZwE7uL07k .error-icon{fill:#552222;}#mermaid-svg-s8dS99DZwE7uL07k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-s8dS99DZwE7uL07k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-s8dS99DZwE7uL07k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-s8dS99DZwE7uL07k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-s8dS99DZwE7uL07k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-s8dS99DZwE7uL07k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-s8dS99DZwE7uL07k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-s8dS99DZwE7uL07k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-s8dS99DZwE7uL07k .marker.cross{stroke:#333333;}#mermaid-svg-s8dS99DZwE7uL07k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-s8dS99DZwE7uL07k p{margin:0;}#mermaid-svg-s8dS99DZwE7uL07k .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-s8dS99DZwE7uL07k .cluster-label text{fill:#333;}#mermaid-svg-s8dS99DZwE7uL07k .cluster-label span{color:#333;}#mermaid-svg-s8dS99DZwE7uL07k .cluster-label span p{background-color:transparent;}#mermaid-svg-s8dS99DZwE7uL07k .label text,#mermaid-svg-s8dS99DZwE7uL07k span{fill:#333;color:#333;}#mermaid-svg-s8dS99DZwE7uL07k .node rect,#mermaid-svg-s8dS99DZwE7uL07k .node circle,#mermaid-svg-s8dS99DZwE7uL07k .node ellipse,#mermaid-svg-s8dS99DZwE7uL07k .node polygon,#mermaid-svg-s8dS99DZwE7uL07k .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-s8dS99DZwE7uL07k .rough-node .label text,#mermaid-svg-s8dS99DZwE7uL07k .node .label text,#mermaid-svg-s8dS99DZwE7uL07k .image-shape .label,#mermaid-svg-s8dS99DZwE7uL07k .icon-shape .label{text-anchor:middle;}#mermaid-svg-s8dS99DZwE7uL07k .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-s8dS99DZwE7uL07k .rough-node .label,#mermaid-svg-s8dS99DZwE7uL07k .node .label,#mermaid-svg-s8dS99DZwE7uL07k .image-shape .label,#mermaid-svg-s8dS99DZwE7uL07k .icon-shape .label{text-align:center;}#mermaid-svg-s8dS99DZwE7uL07k .node.clickable{cursor:pointer;}#mermaid-svg-s8dS99DZwE7uL07k .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-s8dS99DZwE7uL07k .arrowheadPath{fill:#333333;}#mermaid-svg-s8dS99DZwE7uL07k .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-s8dS99DZwE7uL07k .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-s8dS99DZwE7uL07k .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s8dS99DZwE7uL07k .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-s8dS99DZwE7uL07k .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s8dS99DZwE7uL07k .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-s8dS99DZwE7uL07k .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-s8dS99DZwE7uL07k .cluster text{fill:#333;}#mermaid-svg-s8dS99DZwE7uL07k .cluster span{color:#333;}#mermaid-svg-s8dS99DZwE7uL07k div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-s8dS99DZwE7uL07k .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-s8dS99DZwE7uL07k rect.text{fill:none;stroke-width:0;}#mermaid-svg-s8dS99DZwE7uL07k .icon-shape,#mermaid-svg-s8dS99DZwE7uL07k .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-s8dS99DZwE7uL07k .icon-shape p,#mermaid-svg-s8dS99DZwE7uL07k .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-s8dS99DZwE7uL07k .icon-shape .label rect,#mermaid-svg-s8dS99DZwE7uL07k .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-s8dS99DZwE7uL07k .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-s8dS99DZwE7uL07k .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-s8dS99DZwE7uL07k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-s8dS99DZwE7uL07k .prod>*{fill:#E3F2FD!important;stroke:#1E88E5!important;stroke-width:2px!important;color:#0D47A1!important;}#mermaid-svg-s8dS99DZwE7uL07k .prod span{fill:#E3F2FD!important;stroke:#1E88E5!important;stroke-width:2px!important;color:#0D47A1!important;}#mermaid-svg-s8dS99DZwE7uL07k .prod tspan{fill:#0D47A1!important;}#mermaid-svg-s8dS99DZwE7uL07k .cons>*{fill:#E8F5E9!important;stroke:#43A047!important;stroke-width:2px!important;color:#1B5E20!important;}#mermaid-svg-s8dS99DZwE7uL07k .cons span{fill:#E8F5E9!important;stroke:#43A047!important;stroke-width:2px!important;color:#1B5E20!important;}#mermaid-svg-s8dS99DZwE7uL07k .cons tspan{fill:#1B5E20!important;}#mermaid-svg-s8dS99DZwE7uL07k .shared>*{fill:#FFF8E1!important;stroke:#F9A825!important;stroke-width:2px!important;color:#795548!important;}#mermaid-svg-s8dS99DZwE7uL07k .shared span{fill:#FFF8E1!important;stroke:#F9A825!important;stroke-width:2px!important;color:#795548!important;}#mermaid-svg-s8dS99DZwE7uL07k .shared tspan{fill:#795548!important;} Consumer
Producer
写入 buffertail
tail.store(release)
读取 bufferhead
head.store(release)
Shared

(Ring Buffer / Memory)

生产者独占 tail 的修改权,消费者独占 head 的修改权,不存在多个生产者同时竞争写入索引、多个消费者同时竞争读取索引的情况。生产者只需要检查消费者已经释放了哪些槽位,消费者只需要检查生产者已经发布了哪些数据,这正是 Acquire/Release 同步模型的典型适用场景。

4.3 入队操作的内存序

  • tail_:原子变量,生产者维护的下一个可写入位置(队尾)
  • head_:原子变量,消费者维护的下一个读取位置(队头)
  • buffer_[]:环形数组缓冲区
  • advance(idx):环形索引前进函数,advance(x) = (x+1) % ring_size
  • memory_order 是原子内存序,控制 CPU 指令重排

生产者入队的逻辑:

cpp 复制代码
const auto tail = tail_.load(std::memory_order_relaxed);  //从原子变量 `tail_` 读取当前队尾索引,存入局部变量 `tail`
const auto next = advance(tail);  //计算 tail 的下一个位置,环形前进一步

// 获取消费者释放槽位的进度
if (next == head_.load(std::memory_order_acquire)) {  //如果 `next == head` → 环形缓冲区已满,没有空闲槽位,入队失败
    return false;
}

// 写入数据
buffer_[tail] = value;

// 发布新数据
tail_.store(next, std::memory_order_release);  //release 释放语义存储新的队尾索引

这里包含两个关键同步步骤:

  1. 检查槽位可写性 :生产者使用 Acquire 读取 head,以观察消费者已经发布的槽位释放进度。当读取到消费者 Release 更新的 head 时,能够保证消费者之前对相应槽位的读取已经完成。
  2. 发布写入完成 :先写入数据 buffer_[tail] = value,再通过 Release 更新 tail_。生产者将数据写入发布给消费者,消费者之后读取到对应的 tail 更新,才能安全访问这个槽位。

先写数据,再发布索引;不能将这两个操作随意交换。

4.4 出队操作的内存序

消费者出队使用相反的同步过程:

cpp 复制代码
const auto head = head_.load(std::memory_order_relaxed);

// 获取生产者发布数据的进度
if (head == tail_.load(std::memory_order_acquire)) {
    return false;
}

// 读取数据
value = buffer_[head];

// 释放已经消费的槽位
head_.store(advance(head), std::memory_order_release);

消费者首先 Acquire 读取 tail,判断是否存在已经发布的数据。如果队列非空则读取当前槽位,读取完成后通过 Release 更新 head,向生产者发布该槽位已经可以重新使用。

因此整个设计形成双向同步:

  • 生产者 → 消费者:通过 tail 发布新写入的数据
  • 消费者 → 生产者:通过 head 发布已释放的槽位

五、SPSC 无锁队列实现

下面给出一个可直接编译运行的 SPSC 无锁队列实现。为了便于分析算法的进展保证和异常行为,该版本限定元素类型必须支持默认构造、平凡拷贝赋值和平凡析构,适合整数、简单消息结构和固定大小元数据。

5.1 代码

cpp 复制代码
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <thread>
#include <type_traits>
#include <vector>

template <typename T, std::size_t Capacity>
class SPSCQueue {
    static_assert(Capacity >= 2, "Capacity must be >= 2");
    static_assert(std::is_default_constructible_v<T>, "T must be default constructible");
    static_assert(std::is_trivially_copy_assignable_v<T>, "T must have trivial copy assignment");
    static_assert(std::is_trivially_destructible_v<T>, "T must have trivial destructor");

public:
    // 预留一个空槽位,实际容量为 Capacity - 1
    bool try_push(const T& value) noexcept {
        const auto tail = tail_.load(std::memory_order_relaxed);
        const auto next = advance(tail);

        // 获取消费者释放槽位的进度
        if (next == head_.load(std::memory_order_acquire)) {
            return false;
        }

        // 写入数据
        buffer_[tail] = value;

        // 发布新数据
        tail_.store(next, std::memory_order_release);
        return true;
    }

    bool try_pop(T& value) noexcept {
        const auto head = head_.load(std::memory_order_relaxed);

        // 获取生产者发布数据的进度
        if (head == tail_.load(std::memory_order_acquire)) {
            return false;
        }

        // 读取数据
        value = buffer_[head];

        // 释放已经消费的槽位
        head_.store(advance(head), std::memory_order_release);
        return true;
    }

    bool indices_lock_free() const noexcept {
        return head_.is_lock_free() && tail_.is_lock_free();
    }

private:
    static constexpr std::size_t advance(std::size_t index) noexcept {
        return index + 1 == Capacity ? 0 : index + 1;
    }

    // 预分配存储空间
    std::array<T, Capacity> buffer_{};

    // 分离读写索引,按缓存行对齐,降低 False Sharing 风险
    alignas(64) std::atomic<std::size_t> head_{0};  //指定这个变量的起始地址必须是 64 字节的整数倍
    alignas(64) std::atomic<std::size_t> tail_{0};
};

// ---------------- 测试程序 ----------------

int main() {
    constexpr std::size_t N = 200000;
    SPSCQueue<std::uint64_t, 1024> queue;

    std::vector<std::uint64_t> received;
    received.reserve(N);

    // 单生产者
    std::thread producer([&] {
        for (std::uint64_t i = 0; i < N; ++i) {
            while (!queue.try_push(i)) {
                std::this_thread::yield();
            }
        }
    });

    // 单消费者
    std::thread consumer([&] {
        std::uint64_t value;
        while (received.size() < N) {
            if (queue.try_pop(value)) {
                received.push_back(value);
            } else {
                std::this_thread::yield();
            }
        }
    });

    producer.join();
    consumer.join();

    // 验证 FIFO 顺序
    bool ordered = received.size() == N;
    for (std::size_t i = 0; ordered && i < N; ++i) {
        if (received[i] != i) {
            ordered = false;
        }
    }

    std::cout
        << "FIFO check: "
        << (ordered ? "PASS" : "FAIL")
        << '\n';

    std::cout
        << "Index atomics lock-free: "
        << (queue.indices_lock_free() ? "yes" : "no")
        << '\n';

    return ordered ? 0 : 1;
}

5.2 编译与运行

在 Linux 环境下使用 GCC 编译:

bash 复制代码
g++ -std=c++17 -O2 -pthread main.cpp -o spsc_test
./spsc_test

典型运行输出:

text 复制代码
FIFO check: PASS
Index atomics lock-free: yes

该实现可通过 AddressSanitizer、UndefinedBehaviorSanitizer 和 ThreadSanitizer 检查,未报告数据竞争与内存错误。但需要强调:测试通过不等于数学上证明算法正确,也不代表所有硬件平台都能提供相同的无锁保证。

5.3 这个实现为什么能够避免数据竞争?

主要依赖三个约束:

  1. 索引单写入者原则 :tail_ 只有生产者更新,head_ 只有消费者更新,因此不需要 CAS 争抢索引写入权。
  2. 槽位所有权不重叠:生产者只会写入未占用的槽位,消费者只有在观察到已发布元素后才会读取;消费者释放槽位之前,生产者不会覆盖对应数据。
  3. Acquire/Release 同步关系 :生产者发布 tail,消费者获取 tail;消费者发布 head,生产者获取 head。这些同步关系保护了普通 buffer_ 元素的跨线程访问。

此外,由于 try_push() 和 try_pop() 不分配内存、不获取锁,也没有 CAS 重试循环,在目标平台原子读写具备有界执行保证时,这类 SPSC 操作能够达到很强的进展性质。

需要特别注意:该队列的构造、析构必须与并发访问协调,销毁之前应确保生产者和消费者已经停止使用它。

六、MPMC 无锁队列:Michael--Scott Queue

SPSC 依赖单生产者、单消费者的所有权约束来简化同步。但如果需要多个生产者和多个消费者同时操作一个队列,问题复杂度会显著提升。

经典的 Michael--Scott Queue(MS Queue) 提供了一个基于链表和 CAS 的非阻塞 MPMC 队列算法

6.1 Dummy Node:哨兵节点

MS Queue 初始化时创建一个 Dummy Node(哨兵节点):

复制代码
初始状态:

Head ───┐
        ▼
      Dummy ───> null
        ▲
Tail ───┘

第一个元素入队后:

复制代码
Head                   Tail
 │                      │
 ▼                      ▼
Dummy ───────────────> Node A ───> null

此时 Head 仍然指向 Dummy,而真正的队首元素是 Head->next。这种设计能够统一空队列和非空队列的处理逻辑,避免边界条件分支。

当 Node A 成功出队后,Head 会移动到 Node A,原 Dummy 节点成为待回收节点。

MS Queue 的 Head 节点是逻辑哨兵,出队时推进 Head,而不是直接把 Head 指向下一个有效元素后立即释放它。

6.2 MPMC 入队原理

多个生产者可能同时读取到相同的 Tail,因此不能直接使用普通指针赋值连接节点。流程如下:
#mermaid-svg-2LBuvwGrpcRt3pFS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2LBuvwGrpcRt3pFS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2LBuvwGrpcRt3pFS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2LBuvwGrpcRt3pFS .error-icon{fill:#552222;}#mermaid-svg-2LBuvwGrpcRt3pFS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2LBuvwGrpcRt3pFS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2LBuvwGrpcRt3pFS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2LBuvwGrpcRt3pFS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2LBuvwGrpcRt3pFS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2LBuvwGrpcRt3pFS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2LBuvwGrpcRt3pFS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2LBuvwGrpcRt3pFS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2LBuvwGrpcRt3pFS .marker.cross{stroke:#333333;}#mermaid-svg-2LBuvwGrpcRt3pFS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2LBuvwGrpcRt3pFS p{margin:0;}#mermaid-svg-2LBuvwGrpcRt3pFS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2LBuvwGrpcRt3pFS .cluster-label text{fill:#333;}#mermaid-svg-2LBuvwGrpcRt3pFS .cluster-label span{color:#333;}#mermaid-svg-2LBuvwGrpcRt3pFS .cluster-label span p{background-color:transparent;}#mermaid-svg-2LBuvwGrpcRt3pFS .label text,#mermaid-svg-2LBuvwGrpcRt3pFS span{fill:#333;color:#333;}#mermaid-svg-2LBuvwGrpcRt3pFS .node rect,#mermaid-svg-2LBuvwGrpcRt3pFS .node circle,#mermaid-svg-2LBuvwGrpcRt3pFS .node ellipse,#mermaid-svg-2LBuvwGrpcRt3pFS .node polygon,#mermaid-svg-2LBuvwGrpcRt3pFS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2LBuvwGrpcRt3pFS .rough-node .label text,#mermaid-svg-2LBuvwGrpcRt3pFS .node .label text,#mermaid-svg-2LBuvwGrpcRt3pFS .image-shape .label,#mermaid-svg-2LBuvwGrpcRt3pFS .icon-shape .label{text-anchor:middle;}#mermaid-svg-2LBuvwGrpcRt3pFS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2LBuvwGrpcRt3pFS .rough-node .label,#mermaid-svg-2LBuvwGrpcRt3pFS .node .label,#mermaid-svg-2LBuvwGrpcRt3pFS .image-shape .label,#mermaid-svg-2LBuvwGrpcRt3pFS .icon-shape .label{text-align:center;}#mermaid-svg-2LBuvwGrpcRt3pFS .node.clickable{cursor:pointer;}#mermaid-svg-2LBuvwGrpcRt3pFS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2LBuvwGrpcRt3pFS .arrowheadPath{fill:#333333;}#mermaid-svg-2LBuvwGrpcRt3pFS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2LBuvwGrpcRt3pFS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2LBuvwGrpcRt3pFS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2LBuvwGrpcRt3pFS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2LBuvwGrpcRt3pFS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2LBuvwGrpcRt3pFS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2LBuvwGrpcRt3pFS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2LBuvwGrpcRt3pFS .cluster text{fill:#333;}#mermaid-svg-2LBuvwGrpcRt3pFS .cluster span{color:#333;}#mermaid-svg-2LBuvwGrpcRt3pFS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2LBuvwGrpcRt3pFS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2LBuvwGrpcRt3pFS rect.text{fill:none;stroke-width:0;}#mermaid-svg-2LBuvwGrpcRt3pFS .icon-shape,#mermaid-svg-2LBuvwGrpcRt3pFS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2LBuvwGrpcRt3pFS .icon-shape p,#mermaid-svg-2LBuvwGrpcRt3pFS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2LBuvwGrpcRt3pFS .icon-shape .label rect,#mermaid-svg-2LBuvwGrpcRt3pFS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2LBuvwGrpcRt3pFS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2LBuvwGrpcRt3pFS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2LBuvwGrpcRt3pFS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-2LBuvwGrpcRt3pFS .startEnd>*{fill:#E8F5E9!important;stroke:#43A047!important;stroke-width:2px!important;color:#1B5E20!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .startEnd span{fill:#E8F5E9!important;stroke:#43A047!important;stroke-width:2px!important;color:#1B5E20!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .startEnd tspan{fill:#1B5E20!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .step>*{fill:#E3F2FD!important;stroke:#1E88E5!important;stroke-width:2px!important;color:#0D47A1!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .step span{fill:#E3F2FD!important;stroke:#1E88E5!important;stroke-width:2px!important;color:#0D47A1!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .step tspan{fill:#0D47A1!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .decision>*{fill:#FFF8E1!important;stroke:#F9A825!important;stroke-width:2px!important;color:#795548!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .decision span{fill:#FFF8E1!important;stroke:#F9A825!important;stroke-width:2px!important;color:#795548!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .decision tspan{fill:#795548!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .help>*{fill:#FFEBEE!important;stroke:#E53935!important;stroke-width:2px!important;color:#B71C1C!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .help span{fill:#FFEBEE!important;stroke:#E53935!important;stroke-width:2px!important;color:#B71C1C!important;}#mermaid-svg-2LBuvwGrpcRt3pFS .help tspan{fill:#B71C1C!important;} Yes
Yes
No
No
开始

  1. 创建新节点 Node
  2. 读取 Tail 和 Tail->next
  3. 验证 Tail 快照是否一致
  4. Tail->next == null ?
  5. CAS 链接 Node
  6. 链接成功?
  7. 尝试更新 Tail
  8. 入队完成
    协助推进 Tail

其中最关键的操作是:

cpp 复制代码
CAS(tail->next, expected_null, new_node);

节点成功链接到链表的 CAS 操作,就是入队的线性化点。而后续修改 Tail 指针,只是优化队尾指针的位置。即使当前线程在链接成功后被暂停,队列中的元素也已经正式入队。

6.3 Helping Mechanism:协助推进机制

考虑两个生产者并发的场景:线程 A 成功把新节点连接到 Tail->next,但还没来得及更新 Tail 就被调度暂停。此时线程 B 也开始入队,如果线程 B 一直等待线程 A 更新 Tail,就会破坏算法的非阻塞进展性质。

因此 MS Queue 引入了 Helping Mechanism(协助推进机制) :线程 B 发现 tail->next != nullptr 时,意味着当前 Tail 指针已经滞后,于是线程 B 可以通过 CAS 协助将 Tail 推进到下一个节点。

这样一来,线程 A 即使暂时停止运行,也不会使其他生产者因为等待它更新 Tail 而无法继续推进队列。这是一种典型的线程之间协助完成共享状态维护的设计思想。

6.4 MPMC 出队原理

多个消费者同时出队时,必须通过 CAS 竞争队头的推进权。步骤是:

首先读取 Head、Tail 及 Head->next,并验证快照的一致性:

  • 如果 head == tail && head->next == nullptr,则队列为空
  • 如果 head == tail 但 head->next != nullptr,说明 Tail 滞后,需要协助推进
  • 否则可以尝试读取下一个节点的数据,再使用 CAS 推进 Head

关键逻辑伪代码:

cpp 复制代码
// 概念伪代码:假设节点生命周期已受到保护
value = next->data;

if (CAS(head, old_head, next)) {
    // 成功取得一个元素
    // old_head 进入安全回收流程
}

出队的线性化点是成功更新 Head 的 CAS 操作。注意必须在 CAS 之前读取目标数据,否则 CAS 成功后,其他消费者可能继续推进队头,使原目标节点进入回收流程。

但提前读取数据本身还不够,当前线程读取 next->data 时,必须已经通过安全内存回收机制保证节点仍然有效。此外在 CAS 失败的情况下,不应该把尚未确认出队成功的数据错误地交付给业务层。

七、ABA 问题与安全内存回收

对于链表式 MPMC 队列而言,正确使用 CAS 只是第一步,真正困难的问题是节点的生命周期管理。

7.1 ABA 问题的本质

假设线程 A 读取到 head = A,接着线程 A 被调度暂停。在线程 A 暂停期间,其他线程修改了队列结构,并且节点 A 的内存地址在释放后重新分配给了另一个节点。

于是可能出现:

复制代码
线程 A 观察到 Head = A
   │
   ▼
线程 A 暂停
   │
   ▼
其他线程推进队列,节点 A 被释放
   │
   ▼
相同地址被重新分配为新节点
   │
   ▼
Head 地址再次变为 A
   │
   ▼
线程 A 的 CAS 可能错误成功

这就是典型的 ABA 问题。它的本质是:CAS 仅比较了当前的表示值,却不能仅凭这个值判断期间是否发生过状态变化。

7.2 Tagged Pointer:指针与版本号

一种常见的 ABA 解决方式是为指针增加版本号(Tagged Pointer):

cpp 复制代码
template <typename T>
struct TaggedPtr {
    T* ptr;
    std::uint64_t version;
};

将原来的 Head = A 扩展为 Head = (A, version = 5),下一次修改后变为 Head = (B, version = 6)。即使之后地址恢复成 A,也会变成 Head = (A, version = 7),不再与原先的 (A, 5) 相同,因此 CAS 可以识别这种变化。

但这里存在两个限制:

  1. 版本号必须按照算法要求正确更新,不能只给 Head 和 Tail 添加版本号,而忽略其他可能发生 ABA 的共享状态。
  2. 版本号可能发生回绕,指针加版本号的复合原子对象也未必能通过硬件原子指令实现无锁更新。

例如 64 位系统中,包含指针和 64 位版本号的结构通常占用 16 字节,但 std::atomic<TaggedPtr> 并不保证在所有平台上都能以无锁方式执行 CAS。应使用 is_lock_free() 检查该原子对象在当前实现上的无锁支持情况。

7.3 ABA 与 Use-After-Free

假设线程 A 获取了节点指针:

cpp 复制代码
Node* current = head.load(...);

随后线程 B 将该节点出队并释放:

cpp 复制代码
delete old_head;

线程 A 恢复后继续执行:

cpp 复制代码
auto next = current->next.load(...);

此时就可能发生 Use-After-Free(UAF,释放后使用)。

即使为 Head 添加版本号,也不能解决这个问题------因为线程 A 在判断版本号是否变化之前,就可能已经需要解引用旧指针。

Tagged Pointer 主要用于识别共享状态中的 ABA 变化,而安全内存回收负责保证访问节点时其生命周期仍然有效,两者不能直接互相替代。

7.4 Hazard Pointers 危险指针

Hazard Pointers(危险指针) 是一种广泛应用于无锁数据结构的安全内存回收机制。其思想是:线程在访问某个可能被其他线程移除的节点之前,先发布一个 Hazard Pointer,声明自己正在使用该节点。

典型流程:
#mermaid-svg-wUAy8tg6lSXZb8XV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wUAy8tg6lSXZb8XV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wUAy8tg6lSXZb8XV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wUAy8tg6lSXZb8XV .error-icon{fill:#552222;}#mermaid-svg-wUAy8tg6lSXZb8XV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wUAy8tg6lSXZb8XV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wUAy8tg6lSXZb8XV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wUAy8tg6lSXZb8XV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wUAy8tg6lSXZb8XV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wUAy8tg6lSXZb8XV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wUAy8tg6lSXZb8XV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wUAy8tg6lSXZb8XV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wUAy8tg6lSXZb8XV .marker.cross{stroke:#333333;}#mermaid-svg-wUAy8tg6lSXZb8XV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wUAy8tg6lSXZb8XV p{margin:0;}#mermaid-svg-wUAy8tg6lSXZb8XV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wUAy8tg6lSXZb8XV .cluster-label text{fill:#333;}#mermaid-svg-wUAy8tg6lSXZb8XV .cluster-label span{color:#333;}#mermaid-svg-wUAy8tg6lSXZb8XV .cluster-label span p{background-color:transparent;}#mermaid-svg-wUAy8tg6lSXZb8XV .label text,#mermaid-svg-wUAy8tg6lSXZb8XV span{fill:#333;color:#333;}#mermaid-svg-wUAy8tg6lSXZb8XV .node rect,#mermaid-svg-wUAy8tg6lSXZb8XV .node circle,#mermaid-svg-wUAy8tg6lSXZb8XV .node ellipse,#mermaid-svg-wUAy8tg6lSXZb8XV .node polygon,#mermaid-svg-wUAy8tg6lSXZb8XV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wUAy8tg6lSXZb8XV .rough-node .label text,#mermaid-svg-wUAy8tg6lSXZb8XV .node .label text,#mermaid-svg-wUAy8tg6lSXZb8XV .image-shape .label,#mermaid-svg-wUAy8tg6lSXZb8XV .icon-shape .label{text-anchor:middle;}#mermaid-svg-wUAy8tg6lSXZb8XV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wUAy8tg6lSXZb8XV .rough-node .label,#mermaid-svg-wUAy8tg6lSXZb8XV .node .label,#mermaid-svg-wUAy8tg6lSXZb8XV .image-shape .label,#mermaid-svg-wUAy8tg6lSXZb8XV .icon-shape .label{text-align:center;}#mermaid-svg-wUAy8tg6lSXZb8XV .node.clickable{cursor:pointer;}#mermaid-svg-wUAy8tg6lSXZb8XV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wUAy8tg6lSXZb8XV .arrowheadPath{fill:#333333;}#mermaid-svg-wUAy8tg6lSXZb8XV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wUAy8tg6lSXZb8XV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wUAy8tg6lSXZb8XV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wUAy8tg6lSXZb8XV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wUAy8tg6lSXZb8XV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wUAy8tg6lSXZb8XV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wUAy8tg6lSXZb8XV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wUAy8tg6lSXZb8XV .cluster text{fill:#333;}#mermaid-svg-wUAy8tg6lSXZb8XV .cluster span{color:#333;}#mermaid-svg-wUAy8tg6lSXZb8XV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wUAy8tg6lSXZb8XV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wUAy8tg6lSXZb8XV rect.text{fill:none;stroke-width:0;}#mermaid-svg-wUAy8tg6lSXZb8XV .icon-shape,#mermaid-svg-wUAy8tg6lSXZb8XV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wUAy8tg6lSXZb8XV .icon-shape p,#mermaid-svg-wUAy8tg6lSXZb8XV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wUAy8tg6lSXZb8XV .icon-shape .label rect,#mermaid-svg-wUAy8tg6lSXZb8XV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wUAy8tg6lSXZb8XV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wUAy8tg6lSXZb8XV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wUAy8tg6lSXZb8XV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-wUAy8tg6lSXZb8XV .threadA>*{fill:#E3F2FD!important;stroke:#1E88E5!important;stroke-width:2px!important;color:#0D47A1!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .threadA span{fill:#E3F2FD!important;stroke:#1E88E5!important;stroke-width:2px!important;color:#0D47A1!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .threadA tspan{fill:#0D47A1!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .threadB>*{fill:#E8F5E9!important;stroke:#43A047!important;stroke-width:2px!important;color:#1B5E20!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .threadB span{fill:#E8F5E9!important;stroke:#43A047!important;stroke-width:2px!important;color:#1B5E20!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .threadB tspan{fill:#1B5E20!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .decision>*{fill:#FFF8E1!important;stroke:#F9A825!important;stroke-width:2px!important;color:#795548!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .decision span{fill:#FFF8E1!important;stroke:#F9A825!important;stroke-width:2px!important;color:#795548!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .decision tspan{fill:#795548!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .action>*{fill:#F3E5F5!important;stroke:#8E24AA!important;stroke-width:2px!important;color:#4A148C!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .action span{fill:#F3E5F5!important;stroke:#8E24AA!important;stroke-width:2px!important;color:#4A148C!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .action tspan{fill:#4A148C!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .hp>*{fill:#FFEBEE!important;stroke:#E53935!important;stroke-width:2px!important;color:#B71C1C!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .hp span{fill:#FFEBEE!important;stroke:#E53935!important;stroke-width:2px!important;color:#B71C1C!important;}#mermaid-svg-wUAy8tg6lSXZb8XV .hp tspan{fill:#B71C1C!important;} 线程 B
线程 A
是
否
是
否
发布 HP
清除 HP
读取节点指针
发布 Hazard Pointer
重新验证共享指针
已变化?
安全访问受保护节点
清除 Hazard Pointer
节点被移出队列
加入 Retired List
检查危险指针
满足回收条件?
释放节点

其中"发布危险指针之后重新验证共享指针"非常关键,因为线程第一次读取节点指针与发布保护信息之间,也可能有其他线程尝试移除该节点。

Hazard Pointers 通过保护、验证和延迟回收机制,避免节点在仍可能被访问时遭到释放。但其完整实现还涉及正确的内存序、扫描协议和回收策略,不能简单地为每个节点增加一个标志位就视为完成。

7.5 Epoch-Based Reclamation:基于纪元的回收

EBR(Epoch-Based Reclamation,基于纪元的回收) 采用另一种思路:线程进入受保护的并发访问区域时,登记自己所处的 Epoch(纪元)。节点从数据结构中移除后,不立即释放,而是归入对应的退役集合。当系统能够确定所有可能访问该节点的旧临界区都已结束后,再执行内存释放。

与 Hazard Pointers 相比,EBR 通常不需要为每次读取的节点分别维护危险指针,因此在一些场景中同步开销较低。但它也存在代价:如果某个线程长期停留在受保护的临界区,可能导致已经退役的节点长期无法回收。

因此必须根据系统的线程生命周期、暂停行为和内存预算选择合适的回收方案。

7.6 为什么不能简单地定期 delete?

一个常见的错误实现:

cpp 复制代码
std::vector<Node*> garbage;

void gc() {
    for (Node* node : garbage) {
        delete node;
    }
    garbage.clear();
}

即使为 garbage 添加互斥锁,也不能保证安全。因为回收列表的互斥锁只保护列表自身,无法知道其他线程是否仍在访问其中的节点。例如队列每累计 100 个废弃节点就执行一次释放,并不能证明那些节点已经无人使用。

回收阈值是资源管理策略,不是并发内存安全证明。这也是为什么链表式 MPMC 队列的工程实现复杂度,往往远高于普通 SPSC 环形缓冲区。

八、工程中的MPMC 无锁队列

8.1 优先选择经过验证的实现

学习无锁算法非常有价值,但工程中不建议在没有充分验证的情况下自行编写完整 MPMC 队列。原因在于完整实现必须同时处理:并发状态正确性、节点生命周期、内存回收、ABA 问题、异常行为、内存序、进展保证和平台差异。

对于通用 MPMC 场景,可以考虑成熟的工业级实现,例如 Boost.Lockfree 库,它提供了:

  • boost::lockfree::queue:支持多生产者、多消费者操作
  • boost::lockfree::spsc_queue:面向单生产者、单消费者场景

8.2 Boost.Lockfree MPMC 示例

下面给出一个固定容量 MPMC 队列的基本用法:

cpp 复制代码
#include <boost/lockfree/queue.hpp>
#include <iostream>

int main() {
    boost::lockfree::queue<
        int,
        boost::lockfree::capacity<1024>
    > queue;

    // 入队
    if (!queue.push(42)) {
        std::cout << "Queue is full\n";
    }

    // 出队
    int value = 0;
    if (queue.pop(value)) {
        std::cout << "Value: " << value << '\n';
    }

    // 检查内部原子状态的无锁支持
    std::cout
        << "Internal atomics lock-free: "
        << queue.is_lock_free()
        << '\n';

    return 0;
}

使用 capacity<1024> 可以配置固定容量,使入队过程中不需要为了扩展节点数量而动态分配内存。

需要注意:Boost.Lockfree 对元素类型存在特定约束,并不意味着任意复杂 C++ 对象都可以直接作为其存储类型。另外 is_lock_free() 的检查范围由库实现定义,该接口不能穷尽验证每个内部节点上的原子操作。

8.3 有界队列还需要考虑背压

无锁队列不只是一个并发数据结构,还可能是系统数据流的缓冲组件。当生产速率持续大于消费速率时,有界队列迟早会被填满。因此业务层必须定义明确的 Backpressure(背压) 策略。

常见策略包括:等待重试、丢弃最新数据、丢弃最旧数据、批处理以及限制生产速率。

例如高帧率视觉采集场景:

复制代码
Camera 200Hz
     │
     ▼
   SPSC Queue
     │
     ▼
Image Processing

如果图像处理线程长期只能完成 120 FPS,那么无论队列是否使用锁,都无法消除生产与消费之间的速率差。使用无界队列只能推迟问题,最终可能造成持续增长的内存占用和处理延迟;使用有界队列则可以明确资源上限,但必须决定队满时如何处理数据。

对于图像等大对象,更合理的设计往往不是在队列中频繁复制完整图像,而是传递帧描述符或 Buffer Handle。但这仍然需要独立设计缓冲区的所有权、引用计数或安全复用协议,不能因为队列本身无锁就忽略数据生命周期。

九、无锁队列的性能评估

9.1 无锁不一定意味着性能更好

无锁队列通常希望减少互斥锁竞争,但它同样存在开销:多个线程不断更新同一共享索引时,可能引发缓存行所有权迁移和原子读改写竞争;CAS 失败的线程还需要重新读取状态并重试。在竞争非常激烈时,这些行为可能带来较高的 CPU 消耗。

此外,Lock-Free 是并发进展性质,并不是严格的执行时间上界。某个线程虽然不会因为传统互斥锁等待其他线程释放锁,但仍可能长时间 CAS 失败。因此对于实时系统,不能只根据 Lock-Free 标签就认定它满足截止时间要求。

9.2 性能测试指标

指标 说明
Throughput 每秒成功处理的消息数量
Average Latency 单次队列操作平均耗时
P99 / P99.9 Latency 高百分位尾延迟,衡量延迟的稳定性
CPU Usage 运行期间的 CPU 消耗
CAS Failure Rate CAS 失败及重试比例,反映竞争激烈程度
Cache Miss 缓存访问效率,体现数据局部性
Memory Usage 队列本体与回收节点占用的内存总量

测试时还应该分别覆盖低竞争、高竞争、队列经常为空以及队列经常满等典型场景。针对不同并发模型,应分别进行 1P1C、MP1C 和 MPMC 测试,不能使用一个 4 生产者 4 消费者的测试结果,就直接得出某种队列在所有情况下更快的结论。

同时,有锁和无锁版本应该在相同硬件、相同数据类型、相同容量及相同线程亲和性设置下进行公平对比。

9.3 并发正确性测试

性能测试之前,必须优先证明或验证基本正确性。首先可以通过序列号检查队列是否出现元素丢失、重复消费以及违反预期顺序的情况,然后使用 Sanitizer 工具检查常见问题。

AddressSanitizer 检测内存非法访问:

bash 复制代码
g++ -std=c++17 -O1 -g -pthread \
    -fsanitize=address \
    -fno-omit-frame-pointer \
    main.cpp -o test_asan

ThreadSanitizer 检测数据竞争:

bash 复制代码
g++ -std=c++17 -O1 -g -pthread \
    -fsanitize=thread \
    main.cpp -o test_tsan

UndefinedBehaviorSanitizer 检测未定义行为:

bash 复制代码
g++ -std=c++17 -O1 -g -pthread \
    -fsanitize=undefined \
    main.cpp -o test_ubsan

这些工具可以辅助发现内存非法访问、数据竞争和部分未定义行为。但需要注意,TSan 和 ASan 通常建议分别构建运行;并且这些工具不能替代线性化证明或穷举所有线程交错。

对于真正复杂的 MPMC 实现,还应该进行长时间压力测试、线程随机暂停、队列容量边界测试,并在不同 CPU 架构和编译优化配置下验证。

并发测试的通过,只能说明当前测试尚未发现问题,不能据此证明无锁算法在所有线程交错下正确。

总结

无锁队列的关键是通过严格的并发协议,保证多个线程在不依赖传统互斥锁的情况下安全访问和修改共享数据结构。

从实现原理来看:

  • SPSC 环形队列 依赖单生产者、单消费者的所有权约束,以及 Acquire/Release 建立的数据发布与槽位复用同步,实现简单且性能可预测。
  • MPMC 链表队列 则需要通过 CAS 竞争共享状态,利用 Helping Mechanism 保证其他线程能够协助系统推进,并通过安全内存回收机制管理节点生命周期。

在实际工程中,建议遵循以下原则:

  1. 优先明确生产者与消费者数量,而不是直接选择功能最复杂的队列。
  2. 如果是固定容量的 SPSC 数据传输链路,可以优先评估 Ring Buffer。
  3. 如果需要多个线程并发生产或消费,应优先使用经过验证的 MPSC/MPMC 实现,并确认其进展保证、元素类型限制和内存管理策略。
  4. 如果系统并发度不高,或者需要复杂阻塞等待、取消、背压和生命周期管理,基于互斥锁与条件变量的队列仍然是完全合理的方案。

最后需要牢记三个结论:

  • 原子性 解决单次共享状态更新不可分割的问题
  • 内存模型 解决线程之间的同步与有序性问题
  • 安全内存回收 解决被移除节点何时能够销毁的问题

真正成熟的无锁队列,不仅要保证没有数据竞争,还必须能够清晰解释线性化点、进展保证、节点生命周期和具体的性能适用边界。

相关推荐
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】ChatServer 路由规则绑定
网络·c++·人工智能·学习·ai·架构
邪修king1 小时前
Re:Linux系统篇(三十六):进程间通信Chapter3:从匿名管道到命名管道:Linux 进程间通信实战(FIFO 深度解析)
linux·运维·服务器
王老师青少年编程1 小时前
2025年6月GESP真题及题解(C++一级): 选择题和判断题(题解)
c++·真题·gesp·答案·csp·一级·信奥赛
Rabitebla1 小时前
【Linux】 权限篇(三):深入理解 umask 位运算与粘滞位的设计思想
java·linux·服务器·c++·笔记·学习
java_nnnn1 小时前
Linux 学习第一篇:从零认识 Linux,使用 WSL + Ubuntu 搭建学习环境
linux·运维·学习
风翼靓崽2 小时前
磁盘逻辑卷扩容
linux·运维·服务器
Cccp.1232 小时前
【leetcode】(十四)从暴力递归到动态规划
数据结构·算法·leetcode·动态规划
zincsweet2 小时前
Linux网络编程入门(二):从 UDP 回显服务器到多人聊天室,三个项目的渐进式实践
linux·服务器·网络