跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

C++实现线程安全的双缓冲队列 _ 原子变量与内存屏障应用【源码】

std::queue+std::mutex不够快,因其每次push/pop均需加锁、唤醒和上下文切换;双缓冲队列通过两个独立缓冲区解耦生产与消费,仅在满/空切换时用原子操作同步,将临界区从每次操作压缩至每N次一次。 为什么 std::queue + std::mutex 不够快? 高吞吐写入场景下,
std::queue
配合
std::mutex
会成为瓶颈:每次
push()
或
pop()
都要加锁、唤醒、上下文切换。双缓冲队列把“生产”和“消费”解耦到两个独立缓冲区,让写线程只操作当前写缓冲,读线程只操作当前读缓冲,仅在缓冲区满/空切换时才同步——这时用原子变量比互斥锁轻量得多。 关键不是“有没有锁”,而是“锁多久”。双缓冲把临界区从每次操作压窄到每 N 次操作一次(N 是缓冲区容量),而切换动作本身必须靠原子操作 + 内存屏障保序。 如何用 std::atomic 实现缓冲区指针切换? 双缓冲本质是两个固定大小的数组(如
std::array
)和一个指向“当前写缓冲”的原子指针:
std::atomic write_buf{&buf_a}
。注意:不能直接用
std::atomic
,因为
Buffer
通常不可平凡复制;也不能用
std::atomic<:unique_ptr>>
,C++20 前不支持。 写线程:先往
write_buf.load(std::memory_order_acquire)
指向的缓冲追加元素;若满,则用
compare_exchange_strong()
尝试将指针切换到另一个缓冲 读线程:类似地操作
read_buf
,但切换条件是“当前读缓冲已空” 切换成功后,旧缓冲进入“待消费”或“待重用”状态,需保证另一方已完全退出该缓冲 —— 这正是
std::memory_order_release
和
std::memory_order_acquire
要解决的 std::memory_order_relaxed 在哪能用? 缓冲区内部计数器(如
size_t write_pos
)可声明为
std::atomic
并用
std::memory_order_relaxed
:因为单个缓冲内,生产者只增、消费者只减,且切换前已通过指针原子交换+屏障确保跨缓冲顺序。此时 relaxed 能避免不必要的 CPU 内存栅栏指令,提升吞吐。 立即学习 “ C++免费学习笔记(深入) ”; 但以下情况绝不能用 relaxed: C知道 CSDN推出的一款AI技术问答工具 下载 缓冲区指针切换(必须用
acquire/release
或
seq_cst
) 写入元素后更新
write_pos
之前,必须确保元素内容已对读线程可见(需 store-release 配合 load-acquire) 判断缓冲是否满/空时,
write_pos
和
read_pos
的读取需有同步语义,不能各自 relaxed 后做无序比较 实际踩坑:ABA 问题与虚假唤醒 双缓冲本身不涉及链表指针重用,所以 ABA 在缓冲区指针层面不会发生 —— 你只在两个地址间切来切去,没有“释放后重分配再比较”过程。但若引入回收机制(比如把读完的缓冲放回空闲池),就可能触发 ABA:线程 A 看到指针是 X,挂起;线程 B 把 X 缓冲归还、又被线程 C 重新分配为新写缓冲;A 恢复后仍认为 X 是旧缓冲,出错。 解决方案很简单:不用指针,改用带版本号的
std::atomic
,高 32 位存缓冲索引,低 32 位存版本号,每次分配+1;或者直接禁止缓冲复用,用固定两块内存 + 循环等待。 虚假唤醒则常出现在“等待缓冲非空”逻辑里:读线程发现
read_buf->size() == 0
,本该等,但若用
std::condition_variable
就得配锁 —— 这反而破坏了双缓冲的轻量初衷。更自然的做法是:用自旋 +
std::this_thread::yield()
,或结合
std::atomic_flag
做忙等通知,避免进内核态。 真正难处理的是缓冲区边界与内存对齐:如果
T
含虚函数或非 trivial 析构,
std::array
的构造/析构必须显式控制,否则双缓冲切换时可能漏调用。建议优先用 trivially_copyable 类型,或封装为 placement new + 显式调用 destructor 的 RAII 缓冲类。

相关文章