SPSC场景下用std::atomic_size_t配合acquire/release内存序即可,无需锁;capacity必须为2的幂以支持位运算优化;push/pop需基于索引快照判断空满;应返回std::span而非单元素接口或std::string_view。

SPSC 场景下用 std::atomic_size_t 就够了,别上锁
单生产者单消费者(SPSC)是环形缓冲区最常见也最值得优化的场景——生产者只改写索引,消费者只改读索引,天然无竞争。这时候加互斥锁不仅多余,还会引入上下文切换开销,吞吐直接掉一个数量级。
关键不是“无锁”,而是“无竞争”。只要把 write_index 和 read_index 声明为 std::atomic_size_t,再配对使用 load(std::memory_order_acquire) 和 store(std::memory_order_release),就能保证数据写入在索引更新前完成、读取在索引更新后发生。
- 别用普通
size_t+ 手动加锁:锁会破坏实时性,且无法避免伪共享 - 别用
fetch_add直接递增写索引:它返回旧值,但你真正需要的是“确认有空间后再推进”,否则可能写到未初始化内存 - Windows 下慎用
InterlockedCompareExchange:它默认语义等价于__ATOMIC_ACQ_REL,比纯RELEASE重,SPSC 场景里没必要
capacity 必须是 2 的幂,否则位运算失效
环形缓冲区的索引回绕必须高效,index % capacity 在非 2 的幂时无法被编译器优化成位运算,性能损失可达 30% 以上。所以硬性要求:capacity 只能是 1024、4096、65536 这类值。
对应地,用 mask = capacity - 1 替代模运算:buffer[index & mask]。这个操作是零开销的,且能保证缓存友好。
立即学习“C++免费学习笔记(深入)”;
- 构造时不做校验?运行时出错难定位——建议在构造函数里断言
(capacity & (capacity - 1)) == 0 - 用
std::vector管理内存比裸new T[capacity]更安全,支持 move,且避免析构遗漏 - 别用
std::deque或std::queue替代:它们内部多段内存、动态扩容、迭代器失效,实时系统里就是定时炸弹
push/pop 要基于快照判断,不能边读边算
典型错误是先 load 写索引,再 load 读索引,然后立刻用这两个值做减法判断是否满——但两个 load 之间,另一个线程可能已修改其中一个索引,导致计算结果失真。
正确做法是:一次获取两个索引的快照,再基于这对快照做全部判断和写入。利用无符号整数溢出的定义行为(C++ 标准保证),可写出简洁可靠的空满判断:
size_t w = write_index.load(std::memory_order_acquire); size_t r = read_index.load(std::memory_order_acquire); size_t avail = r - w + capacity; // 满时为 0,空时为 capacity if (avail == 0) return false; buffer[w & mask] = item; write_index.store(w + 1, std::memory_order_release);
- 别在
push里用(w - r) == capacity判满:无符号减法下这永远为假 - 别在写入后才检查容量:可能已越界,
buffer[w & mask]会访问非法地址 - 批量 pop 时更要小心:memcpy 前必须确保读索引快照和可读长度一次性算出,否则中间被消费者干扰
用 std::span 暴露连续内存段,别拼碎片
用户真正要的不是 pop() 一个元素,而是“拿一段连续内存交给 send() 或解码器”。环形缓冲区天然可能跨尾部+头部两段,硬塞单元素接口只会逼人反复调用、拼碎片、触发多次系统调用。
get_write_span() 和 get_read_span() 应返回最多两段 std::span<char></char>,调用方按需处理。不暴露裸指针,不触发拷贝,也不依赖底层连续——std::string_view 在这里根本不能用。
- 别返回
std::string_view:它要求内存连续,而环形缓冲区绕圈时必然不满足 - 别自己管理生命周期:用
std::span+std::vector组合,move 安全,RAII 自动清理 - 别在 span 返回前做原子 store:读端必须等数据写完再更新读索引,否则拿到的是脏数据
最易被忽略的点是:内存序约束只作用于索引,不自动保护数据本身。哪怕用了原子变量,如果写入数据和更新索引之间没同步,消费者仍可能读到未完全写入的字节。这不是理论风险,而是真实 crash 来源。



















