Skip to content

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。

核心特性 ​

  1. 角色固定:整个队列只服务一个生产者线程加一个消费者线程,线程角色不可切换。这是它能把性能做到极致的前提。
  2. 无锁且无等待:入队与出队没有比较并交换(CAS)循环,每次操作都在有限的指令步内完成,复杂度 O(1);在 x86 等强内存模型架构上,内存屏障会被编译为空操作,热路径只剩普通的加载、存储与分支。
  3. 通用模板:像 std::queue 一样容纳任意类型并内部管理内存,支持移动语义与原地构造(try_emplace/emplace),避免不必要的拷贝。
  4. 内存行为可预期:构造时预分配连续内存,缓存友好;try_enqueue 保证在初始容量内绝不分配内存,适合实时性敏感的调用路径;enqueue 则在队列满时自动扩容。
  5. 窥视支持: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。

集成方式 ​

  1. 直接包含头文件:把两个 .h 放进项目,#include "readerwriterqueue.h" 即可,无外部依赖、无构建脚本改动,适合快速验证。
  2. CMake FetchContent:在 CMakeLists.txt 里声明后自动拉取与跟踪版本,正式项目推荐。
  3. 系统安装:通过 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 与项目文档整理。)

最近更新

基于 VitePress 构建