readerwriterqueue:C++ SPSC 无锁队列上手
两个固定角色的线程之间单向传递数据——一个线程只生产、一个线程只消费——是并发程序里最常见的协作形态,例如采集线程持续产生样本、处理线程顺序消费。这类场景用单生产者单消费者(SPSC)队列最合适,而手写无锁实现需要正确处理空满区分与内存序配对,出错后往往表现为偶发数据错乱,难以定位。readerwriterqueue 是一个专注 SPSC 场景的 C++11 无锁队列库,两个头文件即插即用,接口与 std::queue 接近。本文介绍它的设计特性、三种队列类型、使用要点与集成方式,适合需要在两个线程间高频传数据、又不想手写无锁结构的开发者。
库概览
- 项目地址:https://github.com/cameron314/readerwriterqueue
- 作者:cameron314(多生产者多消费者库 ConcurrentQueue 的同一作者)
- 许可:简化版 BSD(simplified BSD)
- 核心文件:
readerwriterqueue.h与atomicops.h两个头文件,命名空间moodycamel
atomicops.h 是跨编译器的原子操作与内存屏障抽象层,分别对接 GCC/Clang 的 __atomic_thread_fence、MSVC 的 _ReadWriteBarrier 与 ICC 的内建函数,保证不同编译器下内存序语义一致;队列本体的全部逻辑在 readerwriterqueue.h。
核心特性
- 角色固定:整个队列只服务一个生产者线程加一个消费者线程,线程角色不可切换。这是它能把性能做到极致的前提。
- 无锁且无等待:入队与出队没有比较并交换(CAS)循环,每次操作都在有限的指令步内完成,复杂度 O(1);在 x86 等强内存模型架构上,内存屏障会被编译为空操作,热路径只剩普通的加载、存储与分支。
- 通用模板:像
std::queue一样容纳任意类型并内部管理内存,支持移动语义与原地构造(try_emplace/emplace),避免不必要的拷贝。 - 内存行为可预期:构造时预分配连续内存,缓存友好;
try_enqueue保证在初始容量内绝不分配内存,适合实时性敏感的调用路径;enqueue则在队列满时自动扩容。 - 窥视支持:
peek()允许消费者查看队首元素而不出队。
三种队列类型
| 类别 | 形态 | 关键接口 |
|---|---|---|
ReaderWriterQueue<T> | 基础版,可扩容 | enqueue、try_enqueue、try_dequeue、peek |
BlockingReaderWriterQueue<T> | 阻塞消费 | 基础版全部接口 + wait_dequeue、wait_dequeue_timed |
BlockingReaderWriterCircularBuffer<T> | 固定容量,两侧可阻塞 | wait_enqueue、wait_enqueue_timed 与阻塞出队接口 |
- 基础版:
enqueue满时自动扩容;try_enqueue只使用预分配容量,满则返回 false,全程不分配内存。两者配合可以做到「平时走快速路径,异常时降级处理」。 - 阻塞版:消费者在队列为空时可以用
wait_dequeue挂起等待,wait_dequeue_timed支持超时,省去手写轮询。 - 环形缓冲版:固定容量、从不扩容,生产者在队列满时也可以阻塞等待(
wait_enqueue),适合要求内存用量有上界的场景。
使用要点
编译器要求:C++11 及以上;官方支持 MSVC 2010+、GCC 4.7+(4.6 因原子 fence 相关缺陷被明确排除)、ICC 13+,其余符合 C++11 标准的编译器一般可用。
阻塞接口的生命周期约束:wait_dequeue 阻塞期间销毁队列是未定义行为。使用阻塞版本时,要么保证队列最终一定会有新元素(或让队列具有静态生存期),要么用 wait_dequeue_timed 控制等待上界,并在退出路径上确保没有线程仍在等待。
平台要求:依赖对齐的整数与指针访问具有原子性,适用于 x86、ARM、PowerPC 等主流架构;弱内存序的 DEC Alpha 官方明确不支持。
SPSC 是硬约束:队列不支持多个生产者或多个消费者,两个生产者同时 enqueue 是数据竞争。多对多场景应改用同一作者的 moodycamel ConcurrentQueue。
集成方式
- 直接包含头文件:把两个
.h放进项目,#include "readerwriterqueue.h"即可,无外部依赖、无构建脚本改动,适合快速验证。 - CMake FetchContent:在
CMakeLists.txt里声明后自动拉取与跟踪版本,正式项目推荐。 - 系统安装:通过 CMake 安装到系统目录后,像标准库一样
#include <readerwriterqueue/readerwriterqueue.h>。
什么时候选它
- 两个线程、单向高频传数据:首选。接口小、行为可预期、头文件即插即用。
- 想理解它为什么快:SPSC 的「读写索引各归一个线程」性质与 acquire/release 内存序配对是全部核心,《无锁环形队列设计与实现(SPSC 与 MPMC)》一文有从零手写的完整讨论;对照阅读会发现这个库的每条设计都对应手写实现里的一个取舍。
- 多生产者或多消费者:改用 moodycamel
ConcurrentQueue,不要尝试在 SPSC 队列上 workaround。
要点
- SPSC 专用:一个生产者线程、一个消费者线程,角色固定;多对多场景用
ConcurrentQueue。 try_enqueue不分配内存、失败即返回 false,实时路径用它;enqueue满时自动扩容。- 入队出队无 CAS 循环、O(1),强内存模型架构上内存屏障近乎免费。
- 阻塞版
wait_dequeue等待期间销毁队列是未定义行为,退出路径要保证没有线程仍在等待。 - 集成零门槛:直接拷贝头文件即可起步,正式项目用 CMake FetchContent 管理版本。
(内容依据 readerwriterqueue 官方 README 与项目文档整理。)