compare_exchange_weak适合做计数锁底层操作,因其编译为单条cas指令、失败不阻塞且开销更低,契合“尝试获取、失败即让出”的语义;计数锁用原子整数编码状态:0为空闲,>0为占用且值等于重入深度;try_lock和unlock均需基于compare_exchange_weak的乐观循环实现,避免aba与状态错乱。

compare_exchange_weak 为什么适合做计数锁的底层操作
因为 compare_exchange_weak 在多数硬件上编译为单条 CAS 指令(如 x86 的 cmpxchg),失败时不阻塞线程,也不保证重试次数,开销比 compare_exchange_strong 更低——这正契合计数锁“尝试获取、失败即让出”的语义。
它返回 true 表示旧值匹配且更新成功;false 表示值已被其他线程修改,需重新读取当前值再试。这种“乐观重试”模式天然适配锁状态的多线程竞争场景。
计数锁状态如何用 atomic 编码
不能直接把 0 当“未加锁”、1 当“已加锁”——那样就退化成自旋锁,无法支持计数语义。真正需要的是:一个整数表示当前持有锁的线程数(可重入),同时隐含是否被占用。
-
0:完全空闲 -
> 0:已被占用,数值等于当前持有者调用 lock() 的嵌套深度(即重入次数) - 注意:不区分线程 ID,所以这不是线程安全的可重入锁,仅适用于单线程多次 lock / unlock 或明确由同一线程调用的场景
典型错误是试图用 atomic<bool></bool> 或 atomic_flag 实现计数,它们无法表达 >1 的状态,强行 hack 会导致 unlock() 多次后状态错乱。
如何用 compare_exchange_weak 实现 try_lock 和 unlock
核心逻辑是“读-改-写”循环:读当前值 → 判断能否变更 → 尝试 CAS → 失败则重读。关键在于每次 CAS 的预期值(expected)必须是上一次读到的值,否则会因 ABA 问题跳过中间修改。
示例 try_lock():
bool try_lock() {
int expected = count_.load(std::memory_order_acquire);
do {
if (expected == 0) {
// 尝试从 0 → 1
if (count_.compare_exchange_weak(expected, 1,
std::memory_order_acq_rel, std::memory_order_acquire)) {
return true;
}
} else {
// 已被占用:只允许同一线程递增(假设已知当前线程持有)
// 实际中需配合 thread_local 或 owner_id,此处简化
if (count_.compare_exchange_weak(expected, expected + 1,
std::memory_order_acq_rel, std::memory_order_acquire)) {
return true;
}
}
// expected 被 compare_exchange_weak 更新为实际当前值,继续循环
} while (true);
}
注意点:
-
compare_exchange_weak第二个参数是 desired,不是增量;必须显式计算新值 - 失败时
expected被自动更新为当前值,这是循环能继续的前提 - 内存序选
acq_rel是因为 lock 涉及读+写,unlock 用release即可
unlock() 必须检查计数,避免 underflow
直接 --count_ 不行——这不是原子操作,且无法判断是否为 0 后再改。必须用 CAS 确保:只有在值 >1 时才减一;值 ==1 时才置 0。
常见错误写法:count_.fetch_sub(1) —— 它返回减之前的值,但无法原子地判断“减完是否归零”,导致可能把锁状态从 1 错误地变成 0 以外的负数。
正确做法是两段 CAS:
- 先尝试从
n→n-1(n > 1) - 若当前值为
1,则尝试从1→0 - 两者都用
compare_exchange_weak,失败则重读重试
这个分支逻辑容易漏掉,一旦忽略,unlock() 多调用几次就会让 count_ 变成负数,后续所有 try_lock() 都会误判为“已占用”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











