不能直接用 std::atomic 模拟读写锁,因为单个原子整数无法解决“写者等待时新读者闯入”的竞态;必须用双原子变量或单变量多比特编码(如 uint32_t 分高低16位)协同实现状态管理,并严格配对 memory_order_acquire/release 保证内存序。

为什么不能直接用 std::atomic<int></int> 模拟读写锁
因为读写锁的核心语义是:多个读者可并发进入,但写者必须独占;而单纯用一个原子整数(比如计数读者数)无法安全处理“写者等待期间新读者闯入”的竞态。典型错误是写者调用 load() 发现读者数为 0 后,还没来得及 store(1)(标记自己为写者),另一个读者就完成了 fetch_add(1) ——结果写者被永久阻塞,或读者读到脏数据。
真正可行的方案必须满足两个原子约束:读者入场需检查当前无活跃写者且无待写者,写者入场需确保之后不再有新读者能通过检查。这至少需要两个原子变量协同,或一个更复杂的状态编码(如 16 位计数 + 16 位标志)。
std::atomic<uint32_t></uint32_t> 状态编码实现的关键细节
把状态压缩进单个 uint32_t:低 16 位存读者计数(最多 65535),高 16 位中,第 16 位表示“有活跃写者”,第 17 位表示“有等待中的写者”。这样所有状态变更都可通过 compare_exchange_weak 原子完成。
- 读者加锁:
while (true) { auto s = state.load(); if ((s >> 16) == 0 && (s & 0xFFFF) - 写者加锁:
while (true) { auto s = state.load(); if ((s & 0xFFFF) == 0 && (s >> 16) == 0) { if (state.compare_exchange_weak(s, s | 0x10000)) break; } }——注意这里只设“待写”位,真正获得锁要等读者全部退出后再次 CAS 清零并置“活跃写者”位 - 读者释放时需检查是否唤醒写者:
if (state.fetch_sub(1) == 0x10001) state.compare_exchange_strong(tmp, 0x20000);(唤醒逻辑需更严谨,此处仅为示意)
这种编码省 cache line,但易错点在于:CAS 失败后必须重读 s,否则基于过期值计算新状态会导致死锁;另外 fetch_sub 返回的是修改前的值,判断唤醒时机必须用它而非 load 结果。
立即学习“C++免费学习笔记(深入)”;
用 std::atomic_flag + std::atomic<int></int> 的双变量方案更易懂但有性能代价
一个 std::atomic_flag write_pending{ATOMIC_FLAG_INIT} 标记写者已排队,一个 std::atomic<int> reader_count{0}</int> 计数。写者先置 write_pending.test_and_set(),再等 reader_count == 0;读者入场前检查 write_pending.test() 是否为 true。
- 优势:逻辑清晰,调试友好,适合初版验证
- 劣势:读者每次加锁都要多一次 cache line 读(
write_pending),在高读者争用场景下,该 cache line 会成为热点,导致总线流量激增 - 注意:
std::atomic_flag是无锁的,但test_and_set()是写操作,会触发 store-forwarding stall;若写者极少,可改用std::atomic<bool></bool>配合load(memory_order_acquire)减少干扰
实际使用时最容易被忽略的内存序问题
所有涉及状态检查与后续操作(如读共享数据、写共享数据)之间,必须插入恰当的内存屏障。例如:
- 读者成功加锁后,必须用
memory_order_acquire加载状态,才能保证之后读到的共享数据是写者之前用memory_order_release写入的 - 写者释放锁前,对共享数据的修改必须用
memory_order_release,否则读者可能看到部分更新 - 错误示例:
reader_count.fetch_add(1, memory_order_relaxed)—— 这里 relaxed 足够,但后续访问业务数据时若没 acquire,则仍可能乱序
不写明 memory order 默认是 memory_order_seq_cst,安全但慢;过度使用 acquire/release 则可能掩盖真实依赖。最稳妥的做法是:状态变量 CAS 用 memory_order_acq_rel,纯计数用 memory_order_relaxed,业务数据访问前后配对 acquire/release。
真正难的不是写出来,而是验证——用 ThreadSanitizer 跑 10 万次混合读写压测,比手写逻辑推演可靠得多。


















