无锁多生产者多消费者队列的工程实现剖析------算法、内存模型与性能
以单头文件 C++11 库
mpmc::queue为例仓库:https://github.com/liulilittle/mpmc_queue
版本:本文基于仓库当前提交(
53825e3)撰写。文中所有结论均可溯源至源码
include/mpmc/mpmc_queue.hpp与基准程序bench/bench_mpmc.cpp,引用格式为
文件:行号。
摘要
本文对一个无锁多生产者多消费者(MPMC)队列的工程实现进行完整剖析。该队列以
Michael--Scott 链表队列为基础算法,以 hazard pointer(危险指针)作为内存回收
机制,并在此基础上扩展了批量出队、混合阻塞等待、可插拔分配器与事件队列式
停机语义。全文依次讨论:并发队列的语义与进度模型、数据结构与节点布局、入队与
出队路径、阻塞等待策略、内存回收框架及其安全性论证、内存序(memory ordering)
设计、正确性论证、性能基准与分析方法、测试与验证体系,最后给出设计取舍的客观
讨论。性能数据显示:在测量环境中,该实现于低并发时的绝对吞吐低于
std::mutex + std::deque 基线,其价值体现在进度保证而非原始吞吐。
关键词 :无锁队列;多生产者多消费者;Michael--Scott 队列;Hazard Pointer;
内存序;线性化点
目录
- 引言
- 问题定义与背景
- 数据结构
- 入队路径
- 出队路径
- 阻塞等待:混合等待策略
- [内存回收:Hazard Pointer](#内存回收:Hazard Pointer)
- 内存序设计
- 正确性论证
- 性能
- 测试与验证
- 设计取舍与讨论
- 结论
1. 引言
多生产者多消费者(Multi-Producer Multi-Consumer, MPMC)队列是一类并发数据结构,
允许任意数量的生产线程调用入队、任意数量的消费线程调用出队。MPMC 队列广泛出现
于线程池、事件分发、流水线分解等场景。
本文分析的实现具有以下工程特征:
- 单头文件 :全部实现位于
include/mpmc/mpmc_queue.hpp,约 1500 行,严格
C++11(GCC 7.5 起可编译)。 - 模板化 :
mpmc::queue<T, Allocator>,T需满足 noexcept 移动构造、noexcept
移动赋值、不抛异常析构三个编译期约束(mpmc_queue.hpp:112-120的
static_assert)。 - 无锁数据路径 :数据移动(入队、出队)全程不持锁;互斥量仅用于仲裁空闲
消费者的阻塞等待(mpmc_queue.hpp:10-15)。 - 内存回收:hazard pointer 延迟回收,附带全局迟退休链与线程本地节点缓存。
后续章节按"语义模型 → 算法 → 回收 → 正确性 → 性能"的顺序展开。
2. 问题定义与背景
2.1 并发队列的语义要求
对并发对象的语义,业界普遍采用线性化性 (linearizability)作为正确性条件:
每个操作在其实时区间内的某个时刻(线性化点)原子地生效,且全体操作的线性化
次序必须对应某个合法的顺序执行3。对队列而言,合法顺序执行要求先进先出
(FIFO):出队元素的顺序必须与入队顺序一致。
线性化条件用 happens-before 关系可形式化如下2:对操作 A A A、 B B B,
A → B ⟺ A 在实时序上先于 B 开始且 A 的线性化点先于 B 的线性化点 . A \to B \iff A \text{ 在实时序上先于 } B \text{ 开始且 } A \text{ 的线性化点先于 } B \text{ 的线性化点}. A→B⟺A 在实时序上先于 B 开始且 A 的线性化点先于 B 的线性化点.
若每个操作都能被安排一个不与其实时序冲突的线性化点,则该实现是线性化的。
本文第 9.1 节将给出该队列每个操作的线性化点。
2.2 进度保证的层级
并发算法的进度保证通常划分为四个层级2,从弱到强:
| 层级 | 定义 |
|---|---|
| 阻塞型(blocking) | 某线程的延迟或崩溃可能使其他线程无限期停滞(例如持锁线程被抢占)。 |
| 无干扰(obstruction-free) | 任意单线程在最终获得独占执行(其余线程全部静止)时,能在有限步内完成操作;并发竞争下无保证。 |
| 无锁(lock-free) | 系统级进展:任何有限执行中,总有操作在有限步内完成;不存在让全体线程同时无限停滞的执行。 |
| 无等待(wait-free) | 线程级进展:每个操作都在有限步内完成,与调度无关。 |
无锁的实现代价通常高于互斥版本:每次操作需要更多的原子操作与防护成本。
本文第 10 节的性能数据将量化这一代价。
2.3 链表队列的两类经典故障:ABA 与 use-after-free
基于无锁链表的队列必须同时解决两个问题:
- ABA 问题 :CAS 比较的对象(指针地址)在比较与写回之间被释放又被复用,
使 CAS 误以为状态未变。典型危害:出队线程持有节点 X X X, X X X 被回收并重新分配
为新节点,head_地址不变,CAS 成功但语义已错。 - use-after-free(UAF):节点在另一线程仍可能解引用时被释放。
解决方案的共同思路是延迟回收 :节点先退休(retire),仅当可证明无任何线程
仍可能引用它时才释放。hazard pointer 是实现该思路的经典方法4,也是本实现
采用的方法,详见第 7 节。
3. 数据结构
3.1 总体结构
队列主体为单向链表,配两个原子指针 head_ 与 tail_,以及一个哨兵节点
(dummy sentinel)。哨兵节点不承载数据,永远不被出队,其作用是保证
head_/tail_ 永不为空,从而避免空链表时的大量边界分支
(mpmc_queue.hpp:161-168 构造时 head_/tail_ 均指向哨兵)。
#mermaid-svg-SWSnoiFtr6fN3UoR{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-SWSnoiFtr6fN3UoR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SWSnoiFtr6fN3UoR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SWSnoiFtr6fN3UoR .error-icon{fill:#552222;}#mermaid-svg-SWSnoiFtr6fN3UoR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SWSnoiFtr6fN3UoR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SWSnoiFtr6fN3UoR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SWSnoiFtr6fN3UoR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SWSnoiFtr6fN3UoR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SWSnoiFtr6fN3UoR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SWSnoiFtr6fN3UoR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SWSnoiFtr6fN3UoR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SWSnoiFtr6fN3UoR .marker.cross{stroke:#333333;}#mermaid-svg-SWSnoiFtr6fN3UoR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SWSnoiFtr6fN3UoR p{margin:0;}#mermaid-svg-SWSnoiFtr6fN3UoR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SWSnoiFtr6fN3UoR .cluster-label text{fill:#333;}#mermaid-svg-SWSnoiFtr6fN3UoR .cluster-label span{color:#333;}#mermaid-svg-SWSnoiFtr6fN3UoR .cluster-label span p{background-color:transparent;}#mermaid-svg-SWSnoiFtr6fN3UoR .label text,#mermaid-svg-SWSnoiFtr6fN3UoR span{fill:#333;color:#333;}#mermaid-svg-SWSnoiFtr6fN3UoR .node rect,#mermaid-svg-SWSnoiFtr6fN3UoR .node circle,#mermaid-svg-SWSnoiFtr6fN3UoR .node ellipse,#mermaid-svg-SWSnoiFtr6fN3UoR .node polygon,#mermaid-svg-SWSnoiFtr6fN3UoR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SWSnoiFtr6fN3UoR .rough-node .label text,#mermaid-svg-SWSnoiFtr6fN3UoR .node .label text,#mermaid-svg-SWSnoiFtr6fN3UoR .image-shape .label,#mermaid-svg-SWSnoiFtr6fN3UoR .icon-shape .label{text-anchor:middle;}#mermaid-svg-SWSnoiFtr6fN3UoR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SWSnoiFtr6fN3UoR .rough-node .label,#mermaid-svg-SWSnoiFtr6fN3UoR .node .label,#mermaid-svg-SWSnoiFtr6fN3UoR .image-shape .label,#mermaid-svg-SWSnoiFtr6fN3UoR .icon-shape .label{text-align:center;}#mermaid-svg-SWSnoiFtr6fN3UoR .node.clickable{cursor:pointer;}#mermaid-svg-SWSnoiFtr6fN3UoR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SWSnoiFtr6fN3UoR .arrowheadPath{fill:#333333;}#mermaid-svg-SWSnoiFtr6fN3UoR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SWSnoiFtr6fN3UoR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SWSnoiFtr6fN3UoR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SWSnoiFtr6fN3UoR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SWSnoiFtr6fN3UoR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SWSnoiFtr6fN3UoR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SWSnoiFtr6fN3UoR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SWSnoiFtr6fN3UoR .cluster text{fill:#333;}#mermaid-svg-SWSnoiFtr6fN3UoR .cluster span{color:#333;}#mermaid-svg-SWSnoiFtr6fN3UoR 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-SWSnoiFtr6fN3UoR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SWSnoiFtr6fN3UoR rect.text{fill:none;stroke-width:0;}#mermaid-svg-SWSnoiFtr6fN3UoR .icon-shape,#mermaid-svg-SWSnoiFtr6fN3UoR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SWSnoiFtr6fN3UoR .icon-shape p,#mermaid-svg-SWSnoiFtr6fN3UoR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SWSnoiFtr6fN3UoR .icon-shape .label rect,#mermaid-svg-SWSnoiFtr6fN3UoR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SWSnoiFtr6fN3UoR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SWSnoiFtr6fN3UoR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SWSnoiFtr6fN3UoR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} head_(原子指针)
哨兵节点
(无数据)
节点 1
元素 T1
节点 2
元素 T2
节点 3
元素 T3
NULL
tail_(原子指针)
图 1 队列总体结构。出队总是摘取哨兵的后继;哨兵随后由 CAS 推进,被摘下的
旧哨兵进入延迟回收。
三个实例成员分别独占缓存行(alignas(MPMC_CACHE_LINE),
mpmc_queue.hpp:971-973),避免 head_ 与 tail_ 因位于同一缓存行而互相
颠簸(false sharing)。
3.2 节点布局与分配器归属
节点类型(mpmc_queue.hpp:722-736):
cpp
struct node {
std::atomic<node*> next; // 后继;NULL == 链尾
Allocator_owner_t alloc_owner; // 分配该节点的分配器实例
alignas(T) unsigned char storage[sizeof(T)];
};
next:链表后继指针。storage:按alignof(T)对齐的原生字节区,元素 T 原位构造其中。alloc_owner:指向分配该节点的分配器实例 的指针。这是可插拔分配器支持的
基石:多个queue<T, Allocator>实例可能共享同一模板实例化,而回收路径必须
把内存归还给各自 的分配器实例,绝不交叉归还(mpmc_queue.hpp:1011-1032
的alloc_node与1056-1061的free_node均按alloc_owner归属)。
以 64 位平台、T 为 4 字节标量为例,节点大小约为 8 + 8 + 4 = 20 8 + 8 + 4 = 20 8+8+4=20 字节,
按 8 字节对齐后约 24 字节。
4. 入队路径
入队为标准 Michael--Scott 入队循环1(mpmc_queue.hpp:1151-1173 的
push_node,由 push 调用):
- 分配节点(优先复用本线程缓存中属于本分配器实例的节点),原位构造 T
(noexcept 移动构造,不抛异常)。 - 读取
tail_,用HP_TAILhazard pointer 保护之,并重校验tail_未移动
(防 ABA 与过早回收)。 - 若
t->next == NULL:CAS 将新节点链接到链尾------线性化点
(acq_rel 语义,mpmc_queue.hpp:1161-1162)。成功后尽力推进tail_
(compare_exchange_weak(t, n, release)),失败则说明其他生产者已代为推进。 - 若
t->next != NULL:说明tail_滞后,执行"帮助推进"(CAStail_到
t->next),然后重试。 - 成功后调用
wait_cv_.notify_one()唤醒一个阻塞中的消费者(mpmc_queue.hpp:284)。
#mermaid-svg-g24PmadZKi1vJEei{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-g24PmadZKi1vJEei .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-g24PmadZKi1vJEei .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-g24PmadZKi1vJEei .error-icon{fill:#552222;}#mermaid-svg-g24PmadZKi1vJEei .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-g24PmadZKi1vJEei .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-g24PmadZKi1vJEei .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-g24PmadZKi1vJEei .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-g24PmadZKi1vJEei .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-g24PmadZKi1vJEei .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-g24PmadZKi1vJEei .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-g24PmadZKi1vJEei .marker{fill:#333333;stroke:#333333;}#mermaid-svg-g24PmadZKi1vJEei .marker.cross{stroke:#333333;}#mermaid-svg-g24PmadZKi1vJEei svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-g24PmadZKi1vJEei p{margin:0;}#mermaid-svg-g24PmadZKi1vJEei .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-g24PmadZKi1vJEei .cluster-label text{fill:#333;}#mermaid-svg-g24PmadZKi1vJEei .cluster-label span{color:#333;}#mermaid-svg-g24PmadZKi1vJEei .cluster-label span p{background-color:transparent;}#mermaid-svg-g24PmadZKi1vJEei .label text,#mermaid-svg-g24PmadZKi1vJEei span{fill:#333;color:#333;}#mermaid-svg-g24PmadZKi1vJEei .node rect,#mermaid-svg-g24PmadZKi1vJEei .node circle,#mermaid-svg-g24PmadZKi1vJEei .node ellipse,#mermaid-svg-g24PmadZKi1vJEei .node polygon,#mermaid-svg-g24PmadZKi1vJEei .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-g24PmadZKi1vJEei .rough-node .label text,#mermaid-svg-g24PmadZKi1vJEei .node .label text,#mermaid-svg-g24PmadZKi1vJEei .image-shape .label,#mermaid-svg-g24PmadZKi1vJEei .icon-shape .label{text-anchor:middle;}#mermaid-svg-g24PmadZKi1vJEei .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-g24PmadZKi1vJEei .rough-node .label,#mermaid-svg-g24PmadZKi1vJEei .node .label,#mermaid-svg-g24PmadZKi1vJEei .image-shape .label,#mermaid-svg-g24PmadZKi1vJEei .icon-shape .label{text-align:center;}#mermaid-svg-g24PmadZKi1vJEei .node.clickable{cursor:pointer;}#mermaid-svg-g24PmadZKi1vJEei .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-g24PmadZKi1vJEei .arrowheadPath{fill:#333333;}#mermaid-svg-g24PmadZKi1vJEei .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-g24PmadZKi1vJEei .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-g24PmadZKi1vJEei .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g24PmadZKi1vJEei .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-g24PmadZKi1vJEei .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g24PmadZKi1vJEei .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-g24PmadZKi1vJEei .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-g24PmadZKi1vJEei .cluster text{fill:#333;}#mermaid-svg-g24PmadZKi1vJEei .cluster span{color:#333;}#mermaid-svg-g24PmadZKi1vJEei 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-g24PmadZKi1vJEei .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-g24PmadZKi1vJEei rect.text{fill:none;stroke-width:0;}#mermaid-svg-g24PmadZKi1vJEei .icon-shape,#mermaid-svg-g24PmadZKi1vJEei .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g24PmadZKi1vJEei .icon-shape p,#mermaid-svg-g24PmadZKi1vJEei .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-g24PmadZKi1vJEei .icon-shape .label rect,#mermaid-svg-g24PmadZKi1vJEei .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g24PmadZKi1vJEei .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-g24PmadZKi1vJEei .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-g24PmadZKi1vJEei :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否(tail 已移动)
是
否
是
是
失败
成功
否(tail 滞后)
push(v) 入口
分配节点 n
原位构造 T(noexcept move)
n->next = NULL (relaxed)
t = tail_.load (acquire)
HP_TAIL = t(保护 t)
t == tail_ ?
next = t->next.load (acquire)
t == tail_ ?
next == NULL ?
CAS(t->next, NULL, n)
(acq_rel)
线性化点
CAS(tail_, t, n)
尽力推进
清除 HP_TAIL
wait_cv_.notify_one()
返回 true
CAS(tail_, t, next)
帮助推进
图 2 入队流程。push 本身绝不阻塞,且入队全程不触碰互斥量。
两点工程决策值得说明:
- 异常策略 :
push声明为 noexcept。唯一可能遇到的异常是节点分配的
std::bad_alloc;实现刻意不捕获------内存耗尽时任何恢复路径均无意义,
异常将传播至std::terminate(mpmc_queue.hpp:262-286注释)。 - 停机语义 :
stop()之后队列对输入关闭,push返回false而不入队
(mpmc_queue.hpp:265-267)。与stop()竞争的push可能在停机可见前
完成入队,该元素随后照常被消费。
5. 出队路径
5.1 单元素出队(try_pop)
try_pop(mpmc_queue.hpp:323-394)的步骤:
- 读取
head_,用HP_HEAD保护,重校验未变。 - 读取
tail_(仅比较地址,从不解引用,故无需保护)与h->next
(h 已受保护,解引用安全)。 - 关键边界 :若
n == NULL且h == t,判空返回;若n == NULL但
h != t,说明并发者正在排空或入队者尚未推进tail_,重试而非推进
head_。经典 MS 队列的不变量" h ≠ t ⇒ h → n e x t ≠ NULL h \neq t \Rightarrow h \rightarrow next \neq \text{NULL} h=t⇒h→next=NULL"
在本实现中不成立(批量出队可瞬时令tail_悬于已摘除节点,见 5.2),
因此绝不能把head_推进到 NULL(mpmc_queue.hpp:350-360注释详细论证)。 - 若
h == t且n != NULL:tail_滞后,帮助推进后重试。 - 保护
n(HP_NEXT,防其在 head CAS 前被其他消费者摘走并回收),重校验
h == head_,执行 CAS 推进head_------ 线性化点 (mpmc_queue.hpp:378-379)。 - 成功后:noexcept 移动赋值取出值、销毁节点内 T、
retire(h)进入延迟回收。
#mermaid-svg-bc4OlR8MsMxbaRbD{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-bc4OlR8MsMxbaRbD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bc4OlR8MsMxbaRbD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bc4OlR8MsMxbaRbD .error-icon{fill:#552222;}#mermaid-svg-bc4OlR8MsMxbaRbD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bc4OlR8MsMxbaRbD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bc4OlR8MsMxbaRbD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bc4OlR8MsMxbaRbD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bc4OlR8MsMxbaRbD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bc4OlR8MsMxbaRbD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bc4OlR8MsMxbaRbD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bc4OlR8MsMxbaRbD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bc4OlR8MsMxbaRbD .marker.cross{stroke:#333333;}#mermaid-svg-bc4OlR8MsMxbaRbD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bc4OlR8MsMxbaRbD p{margin:0;}#mermaid-svg-bc4OlR8MsMxbaRbD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bc4OlR8MsMxbaRbD .cluster-label text{fill:#333;}#mermaid-svg-bc4OlR8MsMxbaRbD .cluster-label span{color:#333;}#mermaid-svg-bc4OlR8MsMxbaRbD .cluster-label span p{background-color:transparent;}#mermaid-svg-bc4OlR8MsMxbaRbD .label text,#mermaid-svg-bc4OlR8MsMxbaRbD span{fill:#333;color:#333;}#mermaid-svg-bc4OlR8MsMxbaRbD .node rect,#mermaid-svg-bc4OlR8MsMxbaRbD .node circle,#mermaid-svg-bc4OlR8MsMxbaRbD .node ellipse,#mermaid-svg-bc4OlR8MsMxbaRbD .node polygon,#mermaid-svg-bc4OlR8MsMxbaRbD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bc4OlR8MsMxbaRbD .rough-node .label text,#mermaid-svg-bc4OlR8MsMxbaRbD .node .label text,#mermaid-svg-bc4OlR8MsMxbaRbD .image-shape .label,#mermaid-svg-bc4OlR8MsMxbaRbD .icon-shape .label{text-anchor:middle;}#mermaid-svg-bc4OlR8MsMxbaRbD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bc4OlR8MsMxbaRbD .rough-node .label,#mermaid-svg-bc4OlR8MsMxbaRbD .node .label,#mermaid-svg-bc4OlR8MsMxbaRbD .image-shape .label,#mermaid-svg-bc4OlR8MsMxbaRbD .icon-shape .label{text-align:center;}#mermaid-svg-bc4OlR8MsMxbaRbD .node.clickable{cursor:pointer;}#mermaid-svg-bc4OlR8MsMxbaRbD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bc4OlR8MsMxbaRbD .arrowheadPath{fill:#333333;}#mermaid-svg-bc4OlR8MsMxbaRbD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bc4OlR8MsMxbaRbD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bc4OlR8MsMxbaRbD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bc4OlR8MsMxbaRbD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bc4OlR8MsMxbaRbD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bc4OlR8MsMxbaRbD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bc4OlR8MsMxbaRbD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bc4OlR8MsMxbaRbD .cluster text{fill:#333;}#mermaid-svg-bc4OlR8MsMxbaRbD .cluster span{color:#333;}#mermaid-svg-bc4OlR8MsMxbaRbD 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-bc4OlR8MsMxbaRbD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bc4OlR8MsMxbaRbD rect.text{fill:none;stroke-width:0;}#mermaid-svg-bc4OlR8MsMxbaRbD .icon-shape,#mermaid-svg-bc4OlR8MsMxbaRbD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bc4OlR8MsMxbaRbD .icon-shape p,#mermaid-svg-bc4OlR8MsMxbaRbD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bc4OlR8MsMxbaRbD .icon-shape .label rect,#mermaid-svg-bc4OlR8MsMxbaRbD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bc4OlR8MsMxbaRbD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bc4OlR8MsMxbaRbD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bc4OlR8MsMxbaRbD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
是
是(确为空)
否(并发排空)
否
是
否
否
是
失败
成功
try_pop(out) 入口
h = head_.load (acquire)
HP_HEAD = h(保护)
h == head_ ?
t = tail_.load
n = h->next.load
n == NULL ?
h == t ?
返回 false
清除 HP_HEAD
重试
h == t ?
(tail 滞后)
CAS(tail_, t, n)
帮助推进
HP_NEXT = n(保护)
h == head_ ?
CAS(head_, h, n) (acq_rel)
清除 hazard
重试
线性化点
out = move(*n)
销毁节点内 T
retire(h)
返回 true
图 3 单元素出队流程。与经典 MS 出队的差异集中在 n == NULL ∧ h ≠ t
分支:不推进、不放弃,仅重试。
5.2 批量出队(try_pop_batch / pop_batch)
批量出队(mpmc_queue.hpp:417-535)用单次 head CAS 原子摘除至多 max
个元素,使 CAS 与 hazard pointer 成本摊薄到 max 个元素上:
c ˉ k = c CAS + c walk ( k ) + c HP ( k ) k . \bar{c}k = \frac{c{\text{CAS}} + c_{\text{walk}}(k) + c_{\text{HP}}(k)}{k}. cˉk=kcCAS+cwalk(k)+cHP(k).
- 遍历 :从 h 起沿
next步进至多max步,采用滚动防护纪律------解引用某
节点的next前必先以 hazard pointer 保护该节点,且每步重校验h == head_。
只要head_未变,链上节点不可能已被摘除,遍历读到的是一条稳定存活的链
(mpmc_queue.hpp:433-453)。 - 线性化点 :单次
CAS(head_, h, prev)(acq_rel)。prev为批内最后一个
元素节点,CAS 后它成为新head_,其值随后取出;h..last_prev段整体退休
(retire_batch,mpmc_queue.hpp:526)。 - tail 不变量维护 :CAS 成功后,若
tail_落在摘除段 h , p r e v h, prev h,prev 内,须在
段退休之前 将其推进到段后继(prev->next;若无后继则推进到prev自身,
即合法的空队列状态head_ == tail_ == prev),否则tail_将悬挂在即将释放
的节点上(mpmc_queue.hpp:492-510)。经典 MS 不变量不被依赖,原因即在于此。 - 边界 :
max == 0直接返回 0;遍历 count 为 0 且h == t返回 0(确为空);
count 为 0 且h != t重试------此处绝不解引用t(它可能指向本线程或并发者
刚摘除的节点,读取t->next将与回收竞争)。
#mermaid-svg-qsAkVFItLl8Rfe9D{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-qsAkVFItLl8Rfe9D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qsAkVFItLl8Rfe9D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qsAkVFItLl8Rfe9D .error-icon{fill:#552222;}#mermaid-svg-qsAkVFItLl8Rfe9D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qsAkVFItLl8Rfe9D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qsAkVFItLl8Rfe9D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qsAkVFItLl8Rfe9D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qsAkVFItLl8Rfe9D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qsAkVFItLl8Rfe9D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qsAkVFItLl8Rfe9D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qsAkVFItLl8Rfe9D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qsAkVFItLl8Rfe9D .marker.cross{stroke:#333333;}#mermaid-svg-qsAkVFItLl8Rfe9D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qsAkVFItLl8Rfe9D p{margin:0;}#mermaid-svg-qsAkVFItLl8Rfe9D .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qsAkVFItLl8Rfe9D .cluster-label text{fill:#333;}#mermaid-svg-qsAkVFItLl8Rfe9D .cluster-label span{color:#333;}#mermaid-svg-qsAkVFItLl8Rfe9D .cluster-label span p{background-color:transparent;}#mermaid-svg-qsAkVFItLl8Rfe9D .label text,#mermaid-svg-qsAkVFItLl8Rfe9D span{fill:#333;color:#333;}#mermaid-svg-qsAkVFItLl8Rfe9D .node rect,#mermaid-svg-qsAkVFItLl8Rfe9D .node circle,#mermaid-svg-qsAkVFItLl8Rfe9D .node ellipse,#mermaid-svg-qsAkVFItLl8Rfe9D .node polygon,#mermaid-svg-qsAkVFItLl8Rfe9D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qsAkVFItLl8Rfe9D .rough-node .label text,#mermaid-svg-qsAkVFItLl8Rfe9D .node .label text,#mermaid-svg-qsAkVFItLl8Rfe9D .image-shape .label,#mermaid-svg-qsAkVFItLl8Rfe9D .icon-shape .label{text-anchor:middle;}#mermaid-svg-qsAkVFItLl8Rfe9D .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qsAkVFItLl8Rfe9D .rough-node .label,#mermaid-svg-qsAkVFItLl8Rfe9D .node .label,#mermaid-svg-qsAkVFItLl8Rfe9D .image-shape .label,#mermaid-svg-qsAkVFItLl8Rfe9D .icon-shape .label{text-align:center;}#mermaid-svg-qsAkVFItLl8Rfe9D .node.clickable{cursor:pointer;}#mermaid-svg-qsAkVFItLl8Rfe9D .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qsAkVFItLl8Rfe9D .arrowheadPath{fill:#333333;}#mermaid-svg-qsAkVFItLl8Rfe9D .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qsAkVFItLl8Rfe9D .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qsAkVFItLl8Rfe9D .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qsAkVFItLl8Rfe9D .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qsAkVFItLl8Rfe9D .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qsAkVFItLl8Rfe9D .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qsAkVFItLl8Rfe9D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qsAkVFItLl8Rfe9D .cluster text{fill:#333;}#mermaid-svg-qsAkVFItLl8Rfe9D .cluster span{color:#333;}#mermaid-svg-qsAkVFItLl8Rfe9D 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-qsAkVFItLl8Rfe9D .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qsAkVFItLl8Rfe9D rect.text{fill:none;stroke-width:0;}#mermaid-svg-qsAkVFItLl8Rfe9D .icon-shape,#mermaid-svg-qsAkVFItLl8Rfe9D .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qsAkVFItLl8Rfe9D .icon-shape p,#mermaid-svg-qsAkVFItLl8Rfe9D .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qsAkVFItLl8Rfe9D .icon-shape .label rect,#mermaid-svg-qsAkVFItLl8Rfe9D .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qsAkVFItLl8Rfe9D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qsAkVFItLl8Rfe9D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qsAkVFItLl8Rfe9D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否(count==0 且 h!=t)
count > 0
失败
成功
是
否
try_pop_batch(out, max) 入口
h = head_.load
保护 h
滚动防护步进遍历
每步重校验 head_
count == 0 且 h == t ?
返回 0(确为空)
重试(绝不推进 head_ 到 NULL)
CAS(head_, h, prev)
(acq_rel) ------ 线性化点
清除 hazard,重试
tail_ 在摘除段 h..prev 内?
CAS 推进 tail_ 到
prev->next(或 prev)
取出值,销毁节点内 T
retire_batch(h, last_prev)
返回 count
图 4 批量出队流程。关键顺序保证:先推进 tail_,后退休段节点。
6. 阻塞等待:混合等待策略
阻塞版 pop/pop_batch 采用三段式混合等待(mpmc_queue.hpp:1431-1479),
数据路径本身仍是无锁的:
- 自旋阶段 :前 40 次迭代执行
_mm_pause()(x86 下功耗友好的忙等),
超过后改为std::this_thread::yield(),避免活锁与调度饥饿。 - 阻塞阶段 :以
unique_lock持wait_mutex_,在wait_cv_上执行带
谓词 的wait_for(1ms),谓词为:
stopped ∨ ( head ≠ tail ) . \text{stopped} \lor (\text{head} \neq \text{tail}). stopped∨(head=tail). - 唤醒复位 :被唤醒后
backoff归零,重新进入自旋------突发数据可在不触碰
互斥量的情况下被消费。
谓词等待是本设计的正确性核心(mpmc_queue.hpp:1450-1457 注释):
- 谓词在进入等待之前 与每次唤醒之后 都被重新求值,因此与入队竞争的唤醒
不可能丢失(lost wakeup)------push的notify_one即便错过,谓词也会在
下次求值时观察到数据。 wait_for阻塞期间释放wait_mutex_,而 notify 路径从不获取该互斥量,
不存在锁序交互。- 1 ms 超时是最终安全网,即使调度被恶意扰动,等待也有界。
#mermaid-svg-kUc8xVt6koGHNN7C{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-kUc8xVt6koGHNN7C .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kUc8xVt6koGHNN7C .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kUc8xVt6koGHNN7C .error-icon{fill:#552222;}#mermaid-svg-kUc8xVt6koGHNN7C .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kUc8xVt6koGHNN7C .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kUc8xVt6koGHNN7C .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kUc8xVt6koGHNN7C .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kUc8xVt6koGHNN7C .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kUc8xVt6koGHNN7C .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kUc8xVt6koGHNN7C .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kUc8xVt6koGHNN7C .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kUc8xVt6koGHNN7C .marker.cross{stroke:#333333;}#mermaid-svg-kUc8xVt6koGHNN7C svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kUc8xVt6koGHNN7C p{margin:0;}#mermaid-svg-kUc8xVt6koGHNN7C defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-kUc8xVt6koGHNN7C g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-kUc8xVt6koGHNN7C g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-kUc8xVt6koGHNN7C g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-kUc8xVt6koGHNN7C g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-kUc8xVt6koGHNN7C g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-kUc8xVt6koGHNN7C .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-kUc8xVt6koGHNN7C .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-kUc8xVt6koGHNN7C .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-kUc8xVt6koGHNN7C .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-kUc8xVt6koGHNN7C .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-kUc8xVt6koGHNN7C .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-kUc8xVt6koGHNN7C .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-kUc8xVt6koGHNN7C .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kUc8xVt6koGHNN7C .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kUc8xVt6koGHNN7C .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kUc8xVt6koGHNN7C .edgeLabel .label text{fill:#333;}#mermaid-svg-kUc8xVt6koGHNN7C .label div .edgeLabel{color:#333;}#mermaid-svg-kUc8xVt6koGHNN7C .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-kUc8xVt6koGHNN7C .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-kUc8xVt6koGHNN7C .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-kUc8xVt6koGHNN7C .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-kUc8xVt6koGHNN7C .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-kUc8xVt6koGHNN7C .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kUc8xVt6koGHNN7C .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kUc8xVt6koGHNN7C #statediagram-barbEnd{fill:#333333;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kUc8xVt6koGHNN7C .cluster-label,#mermaid-svg-kUc8xVt6koGHNN7C .nodeLabel{color:#131300;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-kUc8xVt6koGHNN7C .note-edge{stroke-dasharray:5;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-note text{fill:black;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram-note .nodeLabel{color:black;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagram .edgeLabel{color:red;}#mermaid-svg-kUc8xVt6koGHNN7C #dependencyStart,#mermaid-svg-kUc8xVt6koGHNN7C #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-kUc8xVt6koGHNN7C .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kUc8xVt6koGHNN7C :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} "进入 pop()"
"try_pop 成功"
"队列为空"
"backoff < 40:_mm_pause()"
"backoff >= 40"
"yield()"
"谓词不成立"
"wait_for(1ms, 谓词)"
"唤醒:有数据"
"stopped 且已排空"
"stopped 且已排空"
TryPop
Return
Spin
Yield
CVWait
图 5 阻塞出队的混合等待状态机。互斥量仅出现在 CVWait 状态,且阻塞期间
已释放。
7. 内存回收:Hazard Pointer
7.1 回收框架
回收遵循"退休---扫描---释放"三步协议:出队成功的线程将不再可达的节点放入
线程本地退休列表 ;当列表大小超过阈值时执行全局扫描 ,释放所有未被任何
线程 hazard pointer 保护的节点(mpmc_queue.hpp:1188-1240)。
扫描阈值(mpmc_queue.hpp:778-779):
K retire = 2 × N max threads × H = 2 × 128 × 3 = 768 , K_{\text{retire}} = 2 \times N_{\text{max threads}} \times H = 2 \times 128 \times 3 = 768, Kretire=2×Nmax threads×H=2×128×3=768,
其中 H = H P _ C O U N T = 3 H = HP\_COUNT = 3 H=HP_COUNT=3(每槽 hazard 指针数)。
7.2 注册表与活动注册表
全局注册表为 128 个缓存行对齐的槽位(hp_slot),每槽含一个 owner 字段
(std::thread::id,默认值表示空闲)与 3 个 hazard 指针 HP_HEAD、
HP_TAIL、HP_NEXT(mpmc_queue.hpp:749-769)。注册表通过
aligned_alloc_raw 手工对齐分配、永不释放 (mpmc_queue.hpp:834-859),
从结构上消除静态对象与 thread_local 对象之间的析构顺序 UB。
扫描 is_protected 需要回答"节点 X 是否被任一活跃线程保护"。若每次扫描全部
128 槽,每个退休节点都要付出一次 128 槽的全扫描。为此引入活动注册表
(compact registry,mpmc_queue.hpp:888-942):只登记当前持有槽位的线程,
is_protected 只扫描活动槽。
#mermaid-svg-cUuLDz3MiFOJRvvY{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-cUuLDz3MiFOJRvvY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cUuLDz3MiFOJRvvY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cUuLDz3MiFOJRvvY .error-icon{fill:#552222;}#mermaid-svg-cUuLDz3MiFOJRvvY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cUuLDz3MiFOJRvvY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cUuLDz3MiFOJRvvY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cUuLDz3MiFOJRvvY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cUuLDz3MiFOJRvvY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cUuLDz3MiFOJRvvY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cUuLDz3MiFOJRvvY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cUuLDz3MiFOJRvvY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cUuLDz3MiFOJRvvY .marker.cross{stroke:#333333;}#mermaid-svg-cUuLDz3MiFOJRvvY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cUuLDz3MiFOJRvvY p{margin:0;}#mermaid-svg-cUuLDz3MiFOJRvvY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cUuLDz3MiFOJRvvY .cluster-label text{fill:#333;}#mermaid-svg-cUuLDz3MiFOJRvvY .cluster-label span{color:#333;}#mermaid-svg-cUuLDz3MiFOJRvvY .cluster-label span p{background-color:transparent;}#mermaid-svg-cUuLDz3MiFOJRvvY .label text,#mermaid-svg-cUuLDz3MiFOJRvvY span{fill:#333;color:#333;}#mermaid-svg-cUuLDz3MiFOJRvvY .node rect,#mermaid-svg-cUuLDz3MiFOJRvvY .node circle,#mermaid-svg-cUuLDz3MiFOJRvvY .node ellipse,#mermaid-svg-cUuLDz3MiFOJRvvY .node polygon,#mermaid-svg-cUuLDz3MiFOJRvvY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cUuLDz3MiFOJRvvY .rough-node .label text,#mermaid-svg-cUuLDz3MiFOJRvvY .node .label text,#mermaid-svg-cUuLDz3MiFOJRvvY .image-shape .label,#mermaid-svg-cUuLDz3MiFOJRvvY .icon-shape .label{text-anchor:middle;}#mermaid-svg-cUuLDz3MiFOJRvvY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cUuLDz3MiFOJRvvY .rough-node .label,#mermaid-svg-cUuLDz3MiFOJRvvY .node .label,#mermaid-svg-cUuLDz3MiFOJRvvY .image-shape .label,#mermaid-svg-cUuLDz3MiFOJRvvY .icon-shape .label{text-align:center;}#mermaid-svg-cUuLDz3MiFOJRvvY .node.clickable{cursor:pointer;}#mermaid-svg-cUuLDz3MiFOJRvvY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cUuLDz3MiFOJRvvY .arrowheadPath{fill:#333333;}#mermaid-svg-cUuLDz3MiFOJRvvY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cUuLDz3MiFOJRvvY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cUuLDz3MiFOJRvvY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cUuLDz3MiFOJRvvY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cUuLDz3MiFOJRvvY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cUuLDz3MiFOJRvvY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cUuLDz3MiFOJRvvY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cUuLDz3MiFOJRvvY .cluster text{fill:#333;}#mermaid-svg-cUuLDz3MiFOJRvvY .cluster span{color:#333;}#mermaid-svg-cUuLDz3MiFOJRvvY 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-cUuLDz3MiFOJRvvY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cUuLDz3MiFOJRvvY rect.text{fill:none;stroke-width:0;}#mermaid-svg-cUuLDz3MiFOJRvvY .icon-shape,#mermaid-svg-cUuLDz3MiFOJRvvY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cUuLDz3MiFOJRvvY .icon-shape p,#mermaid-svg-cUuLDz3MiFOJRvvY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cUuLDz3MiFOJRvvY .icon-shape .label rect,#mermaid-svg-cUuLDz3MiFOJRvvY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cUuLDz3MiFOJRvvY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cUuLDz3MiFOJRvvY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cUuLDz3MiFOJRvvY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 活动注册表(紧凑)
全局注册表(128 槽,64B 对齐,堆分配不释放)
指向活跃槽
指向活跃槽
槽 0: owner + HP_HEAD/HP_TAIL/HP_NEXT
槽 1: owner + HP_HEAD/HP_TAIL/HP_NEXT
槽 2: owner + HP_HEAD/HP_TAIL/HP_NEXT
槽 3 .. 槽 127
count = 2
slots0 = 2
slots1 = 0
图 6 全局注册表与活动注册表。is_protected 仅扫描 count 个活动条目,
成本与活跃线程数成正比而非与 128 成正比。
活动注册表的并发协议(mpmc_queue.hpp:911-942):
- 注册 :先用 acq_rel 的
fetch_add保留唯一索引 k,再以 relaxed 存储发布
槽位索引。扫描者可能读到尚未落地的条目------其陈旧值只会是以下三种之一:
-1(跳过)、另一活跃槽的索引(按其实时 hazard 检查,安全)、已释放槽的
索引(owner 为默认值,跳过)。它不可能 引用即将被解引用的节点:注册线程
只有在slot()返回之后才会写 hazard 指针。 - 注销 :将末位条目移入被移除位置,relaxed 存储先落地,再以 acq_rel
fetch_sub公布缩减后的 count。持有旧 count 的扫描者读到陈旧条目时,只会
得到"活跃槽(保守检查)"或"已释放槽(跳过)",不可能漏掉正在使用的
hazard 指针,故提前回收不可能发生。
7.3 扫描与迟退休链
scan()(mpmc_queue.hpp:1223-1240)先处理全局迟退休链,再遍历本线程退休
列表:is_protected(node) 为真则保留(等下次扫描),为假则送入节点缓存。
迟退休链 (late-retire,mpmc_queue.hpp:1276-1329)是一个 Treiber 栈
(CAS 压栈,无分配):线程退出时仍被其他线程保护的节点会移交到该链,由后续
任意线程的扫描或队列析构继续回收。链本身是进程期数据结构,同样永不释放。
7.4 线程退出
finalize_thread(mpmc_queue.hpp:1346-1372,由 thread_local guard 在
线程退出时调用)按固定顺序执行:
- 先清空自身 hazard 指针 ------否则
scan会把自家退休节点视为"受保护"而
无限保留,造成泄漏;自身节点已不再被任何人引用,提前清空是安全的。 scan()释放不受保护的退休节点。- 仍受他线程保护的节点压入迟退休链。
- 清空节点缓存(每个节点归还给其所属分配器)。
- 从活动注册表注销,重置槽 owner,槽位可被后续线程复用。
7.5 节点缓存
每个(线程,实例化)拥有一组缓存条目 (node, alloc_owner)
(mpmc_queue.hpp:794-813)。入队优先复用 owner 与本实例 &na_ 匹配的缓存
节点(绝不交叉归还);出队回收的节点先进缓存。缓存在容量达到
MPMC_NODE_CACHE + 1(检查条件为 size <= MPMC_NODE_CACHE,即允许到 65 条)
之前不触发真正的 free/deallocate------在推拉平衡的稳态下,缓存条数在 64 与 65
之间振荡,malloc/free 被完全吸收(mpmc_queue.hpp:1037-1041 注释)。
7.6 安全性论证
回收安全性归结为一个不变量:节点 X 仅在 is_protected(X) == false 时被
释放,即没有任何活动槽持有 X。
证明概要4:任何在"读取 X 的地址"与"最后一次使用 X"之间可能解引用 X 的
线程,必在解引用之前以 release 语义把 X 写入自己的 hazard 槽(
hp_store 用 release,mpmc_queue.hpp:1125-1128)。并发的扫描者以 acquire
语义读取各槽(mpmc_queue.hpp:1247-1270),于是要么观察到防护(保留 X),
要么读到 release 存储之前的值------此时 X 尚未被该线程使用。按退休链归纳,
X 的内存绝不可能在任何线程仍可访问它时被归还。
8. 内存序设计
本节归纳各原子操作的内存序选择及其理由(源码对应关系见
mpmc_queue.hpp 内注释与 docs/DESIGN_en.md 第 4 节)。
| 操作 | 内存序 | 理由 |
|---|---|---|
head_/tail_ 读取 |
acquire | 观察到先前的链接/摘除 |
| hazard 指针写入 | release | 他线程的扫描(acquire)须在解引用前看到防护 |
| hazard 指针清除 | relaxed | 只影响扫描结果,不影响正确性 |
next/head_ 上的 CAS |
acq_rel | 线性化点的两侧:读侧 acquire、写侧 release |
stopped_ 写/读 |
release / acquire | 停机握手 |
empty() 读取 |
acquire | 快照一致性 |
active_users_ 增 |
relaxed | 仅计数 |
active_users_ 减 |
release | 与析构线程的 acquire 循环配对(见 9.4) |
在 x86 架构上,load/store 本身有序:acquire 等价于普通 load,release 等价于
普通 store 加编译屏障;CAS 编译为带 LOCK 前缀的 cmpxchg。这意味着
head_/tail_ 的读改写在 x86 上不会产生额外硬件成本(docs/DESIGN_en.md)。
9. 正确性论证
9.1 线性化点
| 操作 | 线性化点 |
|---|---|
push |
tail_->next 上的成功 CAS(acq_rel) |
try_pop 返回元素 |
head_ 上的成功 CAS(acq_rel) |
try_pop 返回空 |
观察到 h == tail_ 且 h->next == NULL |
try_pop_batch |
head_ 上的唯一成功 CAS(acq_rel),整批原子生效 |
stop |
stopped_ 的 release 存储 |
9.2 FIFO 语义
队列以链表链接顺序为序:每个节点只被链接一次,出队线性化点(head CAS)按
链接顺序推进 head_,因此出队顺序即链接顺序即入队线性化顺序。任意的生产者
交错到达序列都收敛到同一 FIFO 次序------这正是线性化性与单 CAS 推进共同保证的。
压力测试按生产者逐项校验 FIFO 以佐证(见第 11 节)。
9.3 边界情况:tail 滞后与并发排空
实现显式处理了经典 MS 不变量失效的两类情形:
- 入队中 :
h->next != NULL且h == t------tail_滞后,帮助推进后重试。 - 排空中 :
h->next == NULL且h != t------并发批量出队或入队者尚未完成
tail_推进。此时既不能判空也不能推进head_,只能重试;重试必收敛:
入队者的 push 会完成tail_的 CAS,批量出队会在摘除后推进tail_。
这两种情形下任何"把 head_/tail_ 推进到 NULL"的处理都会破坏链表结构,
属于已排除的错误路径。
9.4 析构契约
析构流程(mpmc_queue.hpp:208-227):
#mermaid-svg-3ATiAl8aiRzH2hla{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-3ATiAl8aiRzH2hla .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3ATiAl8aiRzH2hla .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3ATiAl8aiRzH2hla .error-icon{fill:#552222;}#mermaid-svg-3ATiAl8aiRzH2hla .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3ATiAl8aiRzH2hla .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3ATiAl8aiRzH2hla .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3ATiAl8aiRzH2hla .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3ATiAl8aiRzH2hla .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3ATiAl8aiRzH2hla .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3ATiAl8aiRzH2hla .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3ATiAl8aiRzH2hla .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3ATiAl8aiRzH2hla .marker.cross{stroke:#333333;}#mermaid-svg-3ATiAl8aiRzH2hla svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3ATiAl8aiRzH2hla p{margin:0;}#mermaid-svg-3ATiAl8aiRzH2hla .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3ATiAl8aiRzH2hla .cluster-label text{fill:#333;}#mermaid-svg-3ATiAl8aiRzH2hla .cluster-label span{color:#333;}#mermaid-svg-3ATiAl8aiRzH2hla .cluster-label span p{background-color:transparent;}#mermaid-svg-3ATiAl8aiRzH2hla .label text,#mermaid-svg-3ATiAl8aiRzH2hla span{fill:#333;color:#333;}#mermaid-svg-3ATiAl8aiRzH2hla .node rect,#mermaid-svg-3ATiAl8aiRzH2hla .node circle,#mermaid-svg-3ATiAl8aiRzH2hla .node ellipse,#mermaid-svg-3ATiAl8aiRzH2hla .node polygon,#mermaid-svg-3ATiAl8aiRzH2hla .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3ATiAl8aiRzH2hla .rough-node .label text,#mermaid-svg-3ATiAl8aiRzH2hla .node .label text,#mermaid-svg-3ATiAl8aiRzH2hla .image-shape .label,#mermaid-svg-3ATiAl8aiRzH2hla .icon-shape .label{text-anchor:middle;}#mermaid-svg-3ATiAl8aiRzH2hla .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3ATiAl8aiRzH2hla .rough-node .label,#mermaid-svg-3ATiAl8aiRzH2hla .node .label,#mermaid-svg-3ATiAl8aiRzH2hla .image-shape .label,#mermaid-svg-3ATiAl8aiRzH2hla .icon-shape .label{text-align:center;}#mermaid-svg-3ATiAl8aiRzH2hla .node.clickable{cursor:pointer;}#mermaid-svg-3ATiAl8aiRzH2hla .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3ATiAl8aiRzH2hla .arrowheadPath{fill:#333333;}#mermaid-svg-3ATiAl8aiRzH2hla .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3ATiAl8aiRzH2hla .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3ATiAl8aiRzH2hla .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3ATiAl8aiRzH2hla .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3ATiAl8aiRzH2hla .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3ATiAl8aiRzH2hla .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3ATiAl8aiRzH2hla .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3ATiAl8aiRzH2hla .cluster text{fill:#333;}#mermaid-svg-3ATiAl8aiRzH2hla .cluster span{color:#333;}#mermaid-svg-3ATiAl8aiRzH2hla 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-3ATiAl8aiRzH2hla .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3ATiAl8aiRzH2hla rect.text{fill:none;stroke-width:0;}#mermaid-svg-3ATiAl8aiRzH2hla .icon-shape,#mermaid-svg-3ATiAl8aiRzH2hla .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3ATiAl8aiRzH2hla .icon-shape p,#mermaid-svg-3ATiAl8aiRzH2hla .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3ATiAl8aiRzH2hla .icon-shape .label rect,#mermaid-svg-3ATiAl8aiRzH2hla .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3ATiAl8aiRzH2hla .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3ATiAl8aiRzH2hla .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3ATiAl8aiRzH2hla :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
~queue()
stopped_ = true (release)
wait_cv_.notify_all()
active_users_ == 0 ?
notify_all() + yield()
drain_and_free()
遍历链表:销毁 T、回收节点
清空线程本地退休列表与节点缓存
清空迟退休链
图 7 析构流程。active_users_ 由每个公共操作的 RAII 守卫 user_guard
维护(进入时 relaxed 加一,退出时 release 减一)。析构线程以 acquire 语义
轮询该计数:一旦读到零,按 release/acquire 配对,所有在途操作均已完整结束,
可以安全释放全部内存。被阻塞的消费者由 notify_all 唤醒 :其谓词观察到
stopped_ 后立即返回,pop/pop_batch 返回 false/0,线程可被 join------
"队列存在时销毁"成为良定义行为。notify_all 在循环内重复调用,覆盖"消费者
正要进入等待时首次通知已发出"的窗口(其谓词随后立即满足)。
与所有 C++ 对象相同,用户线程仍须在队列生命周期结束前停止使用它(join),
唤醒机制的存在意义是让阻塞中的出队得以返回并完成 join
(mpmc_queue.hpp:204-206)。
两点补充:
stopped()与empty()是咨询性查询,不 取user_guard,不得与析构
并发(mpmc_queue.hpp:616-634)。clear()的并发契约比析构更严:须独占访问,不唤醒阻塞等待者(阻塞中的
pop跨clear仍然阻塞),但释放全部资源后队列可继续使用
(mpmc_queue.hpp:656-694)。
10. 性能
10.1 方法学
基准程序 bench/bench_mpmc.cpp 测量 mpmc::queue<int> 与
std::mutex + std::deque<int> 基线(Windows 上即 SRWLOCK 基)的吞吐对比。
每个场景运行 pairs 个生产线程与 pairs 个消费线程,每个生产者推入
items_per_pair 个元素,每个消费者弹出至相同配额(bench_mpmc.cpp:55-86)。
指标为总吞吐:
R = N items T wall = p a i r s × i t e m s _ p e r _ p a i r T wall . R = \frac{N_{\text{items}}}{T_{\text{wall}}} = \frac{pairs \times items\_per\pair}{T{\text{wall}}}. R=TwallNitems=Twallpairs×items_per_pair.
测量环境:Windows,clang++ 17,-O3,x86-64(LOCK CAS 无竞争约 1.8 ns,
八路竞争约 13.8 ns)。绝对数值随机器而异,相对比较是有效信号
(docs/PERF_en.md)。复现命令:bench_mpmc 500000 8。
10.2 结果(500,000 项/对)
| pairs | mpmc items/s | mutex+deque items/s | 加速比 S S S |
|---|---|---|---|
| 1 | 15,484,142 | 54,497,100 | 0.28× |
| 2 | 8,950,764 | 34,208,149 | 0.26× |
| 4 | 3,467,468 | 9,121,836 | 0.38× |
| 8 | 2,270,437 | 4,262,428 | 0.53× |
其中加速比
S ( p ) = R mpmc ( p ) R mutex ( p ) . S(p) = \frac{R_{\text{mpmc}}(p)}{R_{\text{mutex}}(p)}. S(p)=Rmutex(p)Rmpmc(p).
#mermaid-svg-1aW8wtkZaoefv7fe{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-1aW8wtkZaoefv7fe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1aW8wtkZaoefv7fe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1aW8wtkZaoefv7fe .error-icon{fill:#552222;}#mermaid-svg-1aW8wtkZaoefv7fe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1aW8wtkZaoefv7fe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1aW8wtkZaoefv7fe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1aW8wtkZaoefv7fe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1aW8wtkZaoefv7fe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1aW8wtkZaoefv7fe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1aW8wtkZaoefv7fe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1aW8wtkZaoefv7fe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1aW8wtkZaoefv7fe .marker.cross{stroke:#333333;}#mermaid-svg-1aW8wtkZaoefv7fe svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1aW8wtkZaoefv7fe p{margin:0;}#mermaid-svg-1aW8wtkZaoefv7fe :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 吞吐对比(pairs = 1/2/4/8,500k 项/对,Windows clang++17 -O3) 1 2 4 8 60000000 55000000 50000000 45000000 40000000 35000000 30000000 25000000 20000000 15000000 10000000 5000000 0 items/s
图 8 两组柱分别对应 mpmc 与 mutex+deque 的吞吐。在全部测量点,基线均高于
mpmc;该机上、测量范围内未出现交叉点。
10.3 分析
- 低并发下锁基线为何更快 :每次 MS 出队/入队执行 2--4 次原子操作
(load/CAS)+ 2--3 次 hazard 指针写入,而无竞争的 SRWLOCK 获取/释放成本
极低。无锁算法每次操作付出更多原子工作量,其收益体现在锁竞争主导的场景
(更多线程/核心)。 - 伸缩性 :mpmc 从 1 对到 8 对吞吐下降约 6.8 倍,基线下降约 12.8 倍;
随着核数增加,两者的交叉点取决于核心数与互斥量成本。 - 性质说明 :基准只量化原始吞吐,不量化无锁性质本身------该队列不存在
线程阻塞于互斥量、无优先级反转、进展不依赖调度器。
10.4 单线程地板与批量出队
以下两组数据为历史测量 (docs/PERF_en.md 明确标注,仓库内无对应独立
基准,不可从本仓库复现),仅作量级参考:
单线程 push + try_pop(无并发,无原子成本摊销):
| 配置 | ns/op | items/s |
|---|---|---|
| 裸 Michael--Scott(无回收) | ≈28 | ≈35M |
完整 mpmc::queue |
≈44 | ≈23M |
两者相差约 16 ns/op------即 hazard pointer 防护(保护存储、重校验读取、摊销
扫描)的全部代价,换取"无 UAF、无泄漏"保证:
Δ HP = t full − t bare ≈ 16 ns , \Delta_{\text{HP}} = t_{\text{full}} - t_{\text{bare}} \approx 16\,\text{ns}, ΔHP=tfull−tbare≈16ns,
约使单操作成本上升 57%。
批量出队(Linux,g++ 13,-O2,历史数据):1P1C 下单元素 11 M/s → 批 8
18 M/s(约 1.6×);4P4C 下单元素与批 8 均为 5 M/s(无增益)------消费者众多时,
共享的 head CAS 本身就是瓶颈,批量无法再摊薄。结论:批量出队只在"少量消费者
大量取走"(fan-out 排空)时有效。
11. 测试与验证
仓库测试体系分四层:
- 负向编译测试 :
tests/compile_fail.cpp在配置期(try_compile)验证
queue<CopyOnly>被编译期拒绝(CMakeLists.txt:23-31)。 - 单元测试 :
tests/test_mpmc.cpp含 22 个测试函数,覆盖空队列行为、
FIFO 顺序、stop()语义、移动专属类型、线程进出(join 后零未归还分配)、
有状态分配器(无跨实例释放)、批量 FIFO/短填/混合交错、空弹压力
(100 万次空 try_pop、10 万次空 try_pop_batch)、析构唤醒阻塞出队、
clear()三态(空/释放/复用)。 - 压力测试 :
tests/stress_mpmc.cpp以单元素、批量、混合三种消费形态
× 4P4C、1P8C、8P1C 三种规模共 9 个场景,验证多集相等、每生产者 FIFO
与无泄漏收尾。 - 消毒器与长跑 (历史验证记录,
docs/PERF_en.md第 107-137 行,仓库内
无对应目标,需手动编译复现):
| 套件 | 配置 | 结果 |
|---|---|---|
| 60 秒暴力压力 | 8P8C × 每轮 8M 唯一编号值,每轮全序列校验 | 10 轮,97 s,约 84M 操作,全部通过 |
| 大规模压力 | 9 场景 × 2M = 18M 操作 | ASan/UBSan 0 错误 |
| 压力 9 场景 | 200k 项,TSan(g++ 与 clang) | 0 竞争警告 |
| 析构唤醒 | 100 轮 | ASan 通过;TSan 0 警告 |
| stop 唤醒 | 200 轮 × 6 个阻塞消费者 | ASan 通过 |
| 边界套件(7 项) | 线程颠簸、共享实例、64 消费者全阻塞+突发、批量 FIFO、stop 后继续、唤醒往返、混合随机 | 7/7 通过 |
| 编译警告 | -Wall -Wextra,g++ 13 / clang 18 / MSVC |
0 警告 |
本机复现(本次撰写时):
$ ctest --test-dir build --output-on-failure
1/2 Test #1: test_mpmc ...................... Passed 0.42 sec
2/2 Test #2: stress_mpmc .................... Passed 2.03 sec
100% tests passed, 0 tests failed out of 2
12. 设计取舍与讨论
无锁 vs 互斥 。数据说明:本实现于测量环境中、测量范围内,绝对吞吐始终
低于互斥基线。无锁实现的实际价值不在吞吐,而在三类结构性性质:
(1) 任何单个线程无法阻塞其他线程的进展,即使该线程被抢占或停滞;
(2) 不存在优先级反转;(3) 进展不依赖调度器,对实时性敏感场景可预期。
工程选型时应按"是否依赖上述性质"决定,而非"无锁更快"这一未经数据支持
的假设。
hazard pointer 的代价与替代 。hazard pointer 的代价是本实现中可量化最
大的单一项(单线程地板约 16 ns/op)。替代方案包括 epoch-based reclamation
(EBR,如 Crossbeam 采用)、quiescent-state 类方案(RCU 体系)与引用计数。
hazard pointer 的优势在于无全局屏障、无 epoch 推进延迟,劣势在于每次访问的
防护写入。本实现以活动注册表压缩扫描成本、以节点缓存吸收分配成本,均是
对该劣势的针对性缓解。
严格性选择 。若干设计以可验证性换取便利:noexcept 契约把异常面收敛为
"内存耗尽即终止"单一行为;事件队列语义把停机行为收敛为"排空后返回空";
析构唤醒使阻塞出队对销毁安全。这些选择共同缩小了正确性论证的覆盖面,
与第 9 节的论证结构一致。
局限 。包括:MPMC_MAX_THREADS(默认 128)为并发用户上限,超出者自旋
等待槽位;empty() 为咨询性且不允许与析构并发;clear() 要求独占访问;
基准未覆盖真实负载形态(突发、非均匀消费、有界内存场景下的背压)。
13. 结论
本文对一个 C++11 单头文件无锁 MPMC 队列的工程实现做了完整剖析。实现以
Michael--Scott 链表队列为骨架,以 hazard pointer 为回收机制,以显式维护的
tail_ 不变量和"绝不以 NULL 推进指针"的边界处理适配了批量出队带来的不变量
破坏;以谓词化条件变量实现无丢失唤醒的混合阻塞等待;以活动注册表与迟退休
链压缩回收成本;以 active_users_ 计数与 notify_all 循环定义良性的析构
契约。性能数据表明该实现于低并发时低于互斥基线、伸缩衰减较缓,其定位应
理解为"进度保证"而非"绝对吞吐"。全部结论均可从源码行号与基准数据溯源。
参考文献
- Michael, M. M., Scott, M. L. Simple, fast, and practical non-blocking and
blocking concurrent queue algorithms. PODC '96. - Herlihy, M., Shavit, N. The Art of Multiprocessor Programming . Morgan
Kaufmann, 2008. - Herlihy, M. P., Wing, J. M. Linearizability: A correctness condition for
concurrent objects. ACM TOPLAS 12(3), 1990. - Michael, M. M. Hazard pointers: Safe memory reclamation for lock-free
objects. IEEE TPDS 15(6), 2004. - 本项目源码
include/mpmc/mpmc_queue.hpp、bench/bench_mpmc.cpp、
docs/DESIGN_en.md、docs/PERF_en.md,仓库提交53825e3,
仓库地址 https://github.com/liulilittle/mpmc_queue。
许可
本文按 MIT 许可发布,与项目本体一致。