锁实现中如何原子执行不等则递增并采用acquire-release语义?
问题解答
完全可以做到——通过原子操作把「判断锁可用」和「递增计数器」合并成一个原子步骤,再配合acquire-release语义,既能消除竞态保证正确性,又能避免顺序一致语义的全局同步开销,兼顾性能。
原代码的问题根源
原代码里的if(-1 != m_counter)和++m_counter是两个独立操作,中间有空窗期:比如线程A刚判断完计数器不是-1,线程B就把计数器改成了-1,线程A接着执行递增,就会把-1变成0,直接破坏锁的状态。必须把这两步捏成一个不可分割的原子操作,才能解决这个问题。
两种可行实现方式
方式1:用CAS(比较并交换)实现
CAS是原子操作里的常用工具,能帮我们原子地完成「检查当前值是否符合预期,符合就更新」的逻辑:
#include <atomic> struct Lock { void take_lock() { std::int8_t expected; while (true) { // 先轻松加载当前值,后续会验证是否有效 expected = m_counter.load(std::memory_order_relaxed); while (true) { if (expected == -1) break; // 锁被占了,退出内层循环等一等 // 原子验证并更新:如果当前值还是expected(不是-1),就把它加1 // 成功时用acquire语义,确保后续临界区能看到之前所有线程的写入 // 失败时用relaxed就行,因为expected会自动更新成当前实际值 if (m_counter.compare_exchange_weak(expected, expected + 1, std::memory_order_acquire, std::memory_order_relaxed)) { return; // 成功拿到锁,直接返回 } // CAS失败的话,expected已经是最新值了,接着循环检查就行 } // 锁不可用时,自旋等它变回来(用relaxed加载,不搞多余同步) while (m_counter.load(std::memory_order_relaxed) == -1) { } } } std::atomic<int8_t> m_counter{0}; };
方式2:用fetch_add原子递增+返回值判断
fetch_add会原子地给计数器加1,同时返回操作前的旧值,我们可以通过旧值判断这次递增是否有效:
#include <atomic> struct Lock { void take_lock() { std::int8_t old_val; while (true) { // 原子递增,用acquire语义保证后续操作能看到之前的所有写入 old_val = m_counter.fetch_add(1, std::memory_order_acquire); if (old_val != -1) { // 旧值不是-1,说明我们递增前锁是可用的,成功拿到锁 return; } else { // 踩坑了:递增前锁已经被设为-1了,得把计数器减回去 // 用release语义保证这次回滚对其他线程可见 m_counter.fetch_sub(1, std::memory_order_release); // 自旋等锁可用 while (m_counter.load(std::memory_order_relaxed) == -1) { } } } } std::atomic<int8_t> m_counter{0}; };
为啥选acquire-release语义?
- acquire语义:成功拿到锁时用,确保后续临界区的代码能看到所有之前线程释放锁时的写入操作,不会出现数据不一致。
- release语义:回滚错误递增(方式2)或者后续实现解锁时用,确保当前线程的所有修改对其他线程可见。
- relaxed语义:自旋等待时用,只需要感知计数器的变化,不需要额外的内存同步,能最大化性能。
正确性保证
两种方式都把「判断+递增」做成了原子操作:
- 方式1通过CAS确保,只有当计数器确实不是-1的时候,才会完成递增,中间不会被其他线程插一脚修改计数器。
- 方式2通过fetch_add的原子性拿到旧值,一旦发现旧值是-1就回滚,不会留下错误的计数器状态。
这样既解决了原代码的竞态问题,又比顺序一致语义(std::memory_order_seq_cst)少了全局同步的开销,性能更好。
内容的提问来源于stack exchange,提问作者rare77
相关产品推荐
相关产品推荐

