跳到主要内容
C++ 无锁队列(Lock-Free Queue)详解 | 极客日志
C++ 算法
C++ 无锁队列(Lock-Free Queue)详解 综述由AI生成 介绍 C++ 无锁队列的核心概念、设计难点及实现方案。涵盖无锁与无等待的区别、原子操作与内存序的作用,重点解析 ABA 问题、内存回收及虚假共享等挑战。详细对比了 Michael-Scott 链表队列与环形数组队列(SPSC/MPMC)的实现逻辑,分析了优缺点及适用场景,并推荐了 Boost.Lockfree 等生产级库。旨在帮助开发者在高并发场景下选择合适的数据结构。
樱花落尽 发布于 2026/3/28 更新于 2026/5/29 24 浏览C++ 无锁队列(Lock-Free Queue)详解
无锁队列是不依赖互斥锁(mutex)、自旋锁等同步原语 ,仅通过原子操作(Atomic Operation)和内存序(Memory Order) 保证多线程并发安全的队列数据结构。其核心目标是避免锁竞争带来的线程阻塞、上下文切换开销,从而在高并发场景下提升吞吐量和响应延迟。
一、核心概念与基础前提
1.1 无锁(Lock-Free)vs 无等待(Wait-Free)
很多人会混淆这两个概念,需明确区分:
无锁(Lock-Free) :至少有一个线程 能在有限步骤内完成操作(不会所有线程都陷入无限等待),允许个别线程重试。大多数无锁队列属于此类。
无等待(Wait-Free) :所有线程 都能在有限步骤内完成操作,无任何线程会重试。实现难度极高,常见于简单场景(如单生产者单消费者)。
C++ 中无锁队列通常指 Lock-Free 级别,而非严格的 Wait-Free。
1.2 核心依赖:C++ 原子操作与内存序
无锁队列的安全性完全依赖 std::atomic(C++11 引入)和内存序(Memory Order),二者缺一不可:
(1)std::atomic 原子类型
支持原子的读、写、交换(exchange)、比较并交换(compare_exchange_weak/compare_exchange_strong)等操作。
关键操作:compare_exchange_weak(T& expected, T desired)(CAS 操作),核心逻辑是:
若原子变量当前值 == expected,则更新为 desired,返回 true;
否则将 expected 设为原子变量当前值,返回 false。
示例:std::atomic<Node*> head;(原子指针,用于队列头/尾的并发修改)。
(2)内存序(Memory Order)
CPU 可能重排指令,编译器也可能优化指令顺序,导致多线程下数据可见性错误。内存序用于约束指令重排和数据同步,常用类型:
memory_order_relaxed:最弱,仅保证操作本身原子,不约束重排和可见性(仅用于独立无关的原子操作)。
memory_order_acquire:读操作,禁止后续指令重排到该操作之前;保证该操作读取到的数据是其他线程 release 写入的最新值。
memory_order_release:写操作,禁止之前的指令重排到该操作之后;保证该操作写入的数据对其他线程 acquire 可见。
memory_order_seq_cst:最强(默认),保证所有线程看到的原子操作顺序一致,开销最大。
无锁队列的设计核心是用 acquire/release 内存序替代 seq_cst,在保证正确性的前提下优化性能 。
二、无锁队列的核心设计难点
2.1 ABA 问题(最致命)
问题描述
多线程并发修改时,原子变量的值从 A 被改为 B,再改回 A,导致依赖该值的线程误以为'值未变化',从而执行错误操作。
示例(队列头节点删除) :
线程 T1 读取队列头 head = A,准备通过 CAS 删除 A(将 head 改为 A->next);
线程 T2 插入节点 B(head = B,B->next = A),随后删除 B(head = A);
线程 T1 恢复执行,CAS 检查 head == A(条件成立),将 head 改为 A->next,但此时 A 可能已被 T2 回收(悬空指针),或 A->next 已失效,导致崩溃。
解决方案
标记指针(Tagged Pointer) :给指针附加一个版本号(如 std::atomic<std::pair<Node*, uint64_t>>),每次修改指针时版本号递增。CAS 时同时检查指针和版本号,即使指针相同,版本号不同也会拒绝更新。
风险指针(Hazard Pointers) :线程访问节点前,将节点指针存入'风险指针',确保节点被访问期间不会被回收。
基于时代的回收(Epoch-Based Reclamation) :按'时代'划分线程操作,仅回收所有线程都已退出该时代的节点。
2.2 内存回收问题 无锁队列的节点是动态分配的,但不能直接 delete——可能有其他线程仍在访问该节点(如 T1 读取节点后,T2 删除节点并 delete,T1 后续访问会触发野指针)。
解决方案与 ABA 问题重叠(风险指针、时代回收),核心思想是延迟回收 ,确保节点不再被任何线程引用后再释放。
2.3 虚假共享(False Sharing) 队列的 head 和 tail 原子变量若位于同一缓存行(64 字节),多线程并发修改时会导致缓存行频繁失效(缓存颠簸),严重影响性能。
缓存行对齐:用 std::hardware_destructive_interference_size(C++17)将 head 和 tail 分到不同缓存行。
填充无用数据:在 head 和 tail 之间插入足够的占位字节(如 64 字节),强制编译器分开存储。
2.4 多生产者/多消费者(MPMC)同步
单生产者单消费者(SPSC):无锁队列实现最简单,无需处理 head/tail 的并发修改冲突。
多生产者多消费者(MPMC):需通过 CAS 保证 head(出队)和 tail(入队)的原子修改,逻辑更复杂。
三、常见无锁队列实现结构
3.1 Michael-Scott 无锁队列(MPMC,链表型) 由 Michael 和 Scott 于 1996 年提出,是最经典的 MPMC 无锁队列实现,基于单向链表,核心是通过 CAS 原子修改 head 和 tail。
结构设计 template <typename T>
struct Node {
T data;
std::atomic<Node*> next;
Node (const T& val) : data (val), next (nullptr ) {}
};
template <typename T>
class MSQueue {
private :
struct TaggedPtr {
Node<T>* ptr;
uint64_t tag;
TaggedPtr (Node<T>* p = nullptr , uint64_t t = 0 ) : ptr (p), tag (t) {}
bool operator ==(const TaggedPtr& other) const {
return ptr == other.ptr && tag == other.tag;
}
};
std::atomic<TaggedPtr> head;
std::atomic<TaggedPtr> tail;
char padding[std::hardware_destructive_interference_size - sizeof (std::atomic<TaggedPtr>)];
public :
MSQueue () {
Node<T>* dummy = new Node <T>(T{});
head.store ({dummy, 0 }, std::memory_order_relaxed);
tail.store ({dummy, 0 }, std::memory_order_relaxed);
}
~MSQueue ();
};
核心操作:入队(enqueue)
创建新节点;
循环通过 CAS 更新 tail:
读取当前 tail(acquire 内存序,确保看到最新的 tail->next);
若 tail->ptr->next 为 nullptr,尝试将其 CAS 改为新节点(release 内存序);
若 CAS 成功,再将 tail CAS 改为新节点(release 内存序),入队完成;
若失败(其他线程已修改 tail 或 tail->next),重试。
void enqueue (const T& val) {
Node<T>* new_node = new Node <T>(val);
TaggedPtr new_tail{new_node, 0 };
while (true ) {
TaggedPtr current_tail = tail.load (std::memory_order_acquire);
Node<T>* tail_ptr = current_tail.ptr;
std::atomic<Node*>& next = tail_ptr->next;
Node<T>* null_ptr = nullptr ;
if (next.compare_exchange_strong (
null_ptr, new_node,
std::memory_order_release,
std::memory_order_relaxed)) {
TaggedPtr expected_tail{tail_ptr, current_tail.tag};
new_tail.tag = current_tail.tag + 1 ;
tail.compare_exchange_weak (
expected_tail, new_tail,
std::memory_order_release,
std::memory_order_relaxed);
return ;
} else {
Node<T>* next_ptr = next.load (std::memory_order_relaxed);
TaggedPtr expected_tail{tail_ptr, current_tail.tag};
TaggedPtr candidate_tail{next_ptr, current_tail.tag + 1 };
tail.compare_exchange_weak (
expected_tail, candidate_tail,
std::memory_order_release,
std::memory_order_relaxed);
}
}
}
核心操作:出队(dequeue)
循环通过 CAS 更新 head:
读取当前 head 和 tail(acquire 内存序);
若队列空(head->ptr == tail->ptr 且 head->ptr->next 为 nullptr),返回失败;
读取 head->ptr->next(实际数据节点);
尝试将 head CAS 改为该数据节点(版本号递增,release 内存序);
若 CAS 成功,释放原 head->ptr(哨兵节点),返回数据;
若失败,重试。
bool dequeue (T& val) {
while (true ) {
TaggedPtr current_head = head.load (std::memory_order_acquire);
TaggedPtr current_tail = tail.load (std::memory_order_acquire);
Node<T>* head_ptr = current_head.ptr;
Node<T>* tail_ptr = current_tail.ptr;
Node<T>* next_ptr = head_ptr->next.load (std::memory_order_relaxed);
if (current_head == head.load (std::memory_order_relaxed)) {
if (head_ptr == tail_ptr) {
if (next_ptr == nullptr ) {
return false ;
}
TaggedPtr expected_tail{tail_ptr, current_tail.tag};
TaggedPtr candidate_tail{next_ptr, current_tail.tag + 1 };
tail.compare_exchange_weak (
expected_tail, candidate_tail,
std::memory_order_release,
std::memory_order_relaxed);
} else {
if (next_ptr == nullptr ) {
continue ;
}
val = next_ptr->data;
TaggedPtr expected_head{head_ptr, current_head.tag};
TaggedPtr new_head{next_ptr, current_head.tag + 1 };
if (head.compare_exchange_strong (
expected_head, new_head,
std::memory_order_release,
std::memory_order_relaxed)) {
delete head_ptr;
return true ;
}
}
}
}
}
注意 上述是简化版实现 ,未完全解决内存回收(如 delete head_ptr 可能导致其他线程访问失效节点)和复杂 ABA 场景,仅用于理解核心逻辑。生产环境需结合风险指针或时代回收完善。
3.2 环形无锁队列(SPSC/MPMC,数组型) 基于固定大小数组,通过原子索引 head(出队索引)和 tail(入队索引)实现,适合数据量固定、高吞吐场景。
核心特点
单生产者单消费者(SPSC):head 仅由消费者修改,tail 仅由生产者修改,无需 CAS,仅需内存序保证可见性,实现最简单、性能最优。
多生产者多消费者(MPMC):head 和 tail 需通过 CAS 原子修改,需处理索引环绕(取模)和满/空判断。
SPSC 环形无锁队列示例 #include <atomic>
#include <vector>
template <typename T, size_t Size>
class SPSCQueue {
private :
static constexpr size_t CAPACITY = Size;
std::vector<T> buffer;
std::atomic<size_t > head;
char padding1[std::hardware_destructive_interference_size - sizeof (std::atomic<size_t >)];
std::atomic<size_t > tail;
char padding2[std::hardware_destructive_interference_size - sizeof (std::atomic<size_t >)];
public :
SPSCQueue () : buffer (CAPACITY), head (0 ), tail (0 ) {}
bool enqueue (const T& val) {
const size_t current_tail = tail.load (std::memory_order_relaxed);
const size_t next_tail = (current_tail + 1 ) % CAPACITY;
if (next_tail == head.load (std::memory_order_acquire)) {
return false ;
}
buffer[current_tail] = val;
tail.store (next_tail, std::memory_order_release);
return true ;
}
bool dequeue (T& val) {
const size_t current_head = head.load (std::memory_order_relaxed);
if (current_head == tail.load (std::memory_order_acquire)) {
return false ;
}
val = buffer[current_head];
head.store ((current_head + 1 ) % CAPACITY, std::memory_order_release);
return true ;
}
bool empty () const {
return head.load (std::memory_order_acquire) == tail.load (std::memory_order_acquire);
}
bool full () const {
const size_t next_tail = (tail.load (std::memory_order_acquire) + 1 ) % CAPACITY;
return next_tail == head.load (std::memory_order_acquire);
}
};
关键说明
SPSC 队列无需 CAS,因为 head 和 tail 分别由单个线程修改,仅需 acquire/release 保证生产者写入的数据对消费者可见。
队列满/空判断:通过 next_tail == head(满)和 head == tail(空),牺牲一个元素空间(避免满和空的判断冲突)。
性能极高:数组存储无链表节点的动态分配开销,缓存局部性好,适合高频读写场景(如线程池任务队列、日志采集)。
四、无锁队列的优缺点与适用场景
4.1 优点
低延迟 :无锁竞争,避免线程阻塞和上下文切换。
高吞吐 :高并发下,原子操作的开销远小于锁的竞争开销。
无死锁风险 :不依赖锁,自然避免死锁。
4.2 缺点
实现复杂 :需处理 ABA 问题、内存回收、内存序等细节,调试难度大。
低并发场景性能不佳 :原子操作的硬件开销(如 CAS 需 CPU 总线锁定)可能高于互斥锁,低并发时无优势。
内存开销 :链表型无锁队列的节点动态分配、标记指针的版本号都会增加内存开销。
不保证公平性 :线程可能因 CAS 失败频繁重试,导致饥饿(无等待队列可避免)。
4.3 适用场景
高并发、低延迟需求:如金融交易、实时数据处理、高性能服务器。
多线程高频读写:如线程池任务分发、日志队列、消息队列。
避免锁竞争瓶颈:如多生产者多消费者场景,锁队列的竞争会导致吞吐量下降。
4.4 不适用场景
低并发、低频率读写:如偶尔的线程间数据传递,有锁队列(std::queue + std::mutex)更简单、性能相当。
数据量小且固定:普通数组或链表即可满足,无需复杂的无锁设计。
对公平性要求高:无锁队列可能导致部分线程长期重试。
五、C++ 生产级无锁队列库推荐 手动实现无锁队列难度大、易出错,生产环境优先使用成熟库:
Boost.Lockfree :
支持 lockfree::queue(MPMC)、lockfree::stack(MPSC)、lockfree::spsc_queue(SPSC)。
跨平台,稳定性高,支持内存回收(基于风险指针)。
folly::ConcurrentQueue (Facebook Folly):
MPMC 无锁队列,性能优异,支持批量入队/出队、动态扩容。
依赖 Folly 库(需编译安装),功能丰富(如优先级队列、阻塞/非阻塞模式)。
moodycamel::ConcurrentQueue :
轻量级 MPMC 无锁队列,无依赖,单头文件,性能接近 Boost.Lockfree。
支持批量操作、超时等待,适合快速集成。
absl::synchronization::BoundedQueue (Google Abseil):
有界 MPMC 队列,支持无锁模式,集成 Abseil 生态,稳定性高。
#include <boost/lockfree/queue.hpp>
boost::lockfree::queue<int > q (1024 ) ;
q.push (42 );
int val;
q.pop (val);
六、总结 无锁队列是 C++ 高并发编程的核心数据结构,其本质是用原子操作替代锁,用内存序保证数据可见性和指令顺序 。关键难点是 ABA 问题和内存回收,解决方案包括标记指针、风险指针、时代回收等。
实际开发中,优先选择成熟的第三方库(如 Boost.Lockfree、moodycamel),避免重复造轮子;若需自定义实现,建议从 SPSC 环形队列入手(逻辑简单、不易出错),再逐步挑战 MPMC 链表队列。
无锁 ≠ 无开销,原子操作仍有硬件开销,需根据并发强度选择。
内存序是关键,错误的内存序会导致数据竞争和可见性问题。
测试至关重要,需通过压力测试、线程 sanitizer(TSAN)检测数据竞争。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online