读写自旋锁必须用std::atomic整数编码读者计数与写者状态(如低16位存读者数、高位表写持有/等待),禁止单布尔值或分离计数器;所有状态变更须compare_exchange_weak循环重试,并分层选用memory_order:读者计数用relaxed,写状态变更用acquire/release,“写等待”位用acq_rel。

std::atomic 实现读写自旋锁的核心约束
读写自旋锁不能靠 std::atomic_flag 或单个布尔值实现,必须用整数状态编码读/写意图与计数。常见错误是直接复用普通自旋锁的 test_and_set 逻辑——这会导致写优先或读饥饿,根本不是读写锁。
核心约束有三点:
• 写者必须独占,且一旦有写者在等待,新读者不得插入(否则写者永远等不到空窗)
• 读者可并发,但需原子递增/递减计数,且不能在写者活跃时进入
• 状态变量必须能同时表达:当前读者数、是否有活跃写者、是否有等待写者
- 推荐用
std::atomic<int></int>编码:低16位存读者数,高位用 bit 表示写者状态(例如 bit16=写者持有,bit17=写者等待) - 避免用
std::atomic<bool></bool>+ 单独计数器——读写操作无法原子协同,极易出现 ABA 或计数错乱 - 所有状态变更必须用
compare_exchange_weak循环重试,不能依赖fetch_add独立操作
写锁获取为何必须先标记“写等待”再自旋
如果写者上来就尝试把状态设为“写占用”,而此时已有大量读者,它会无限自旋,同时阻塞后续所有读者——这是典型的写饥饿+读者饿死双重问题。
正确做法是分两步:
• 第一步:用 CAS 将“写等待”位设为 1(不改变读者计数),成功即进入第二步;失败说明有其他写者已在等待,继续自旋
• 第二步:等待读者数归零后,再 CAS 尝试获取写权限
- 这要求状态检查逻辑必须读取完整整数值,而非只看某一位——
load(memory_order_acquire)后解析各字段 - 写者释放时,必须先清写标志,再唤醒可能等待的读者或下一个写者(通常靠简单
store(0)即可,因无条件竞争) - Intel CPU 上建议在自旋循环内插入
_mm_pause(),避免过度消耗前端总线带宽
读者加锁时如何避免被“假唤醒”干扰
读者加锁失败,常见错误是看到“有写等待”就立刻放弃或重试——这会导致明明写者还没真正拿到锁,读者就退避,放大抖动。
真实场景中,读者应容忍短暂的写等待:只要当前无活跃写者(读者数 > 0 且写位为 0),就可安全进入;只有当写者已持锁或正处在“写等待+读者数=0”的临界切换点时,才需退让。
- 典型判断逻辑:
if ((state & WRITE_ACTIVE) == 0 && (state & WRITE_PENDING) == 0)才允许读者递增 - 若发现
WRITE_PENDING已置位但读者数非零,仍可抢入——因为写者尚未清空读者,此时读者仍是安全的 - 不要在读者路径里 sleep 或 yield,那会破坏自旋锁“零延迟”的设计前提
memory_order 选错会导致什么具体问题
用 memory_order_relaxed 做所有操作,大概率在 ARM 或旧版 GCC 下出现读写乱序,导致读者看到过期的写者状态;全用 memory_order_seq_cst 则性能断崖下跌,尤其在高核数机器上。
实际应分层选择:
• 读者计数增减:用 memory_order_relaxed(仅本地计数,不涉同步语义)
• 写者状态变更(设置/清除 WRITE_ACTIVE):必须用 memory_order_acquire(加锁)和 memory_order_release(解锁)
• “写等待”位设置:用 memory_order_acq_rel,确保之前读操作不越界、之后写操作不提前
- 漏掉 acquire/release 的最常见现象:写者释放锁后,读者仍读到旧值,持续自旋数毫秒甚至更久
- 在 x86 上部分宽松序看似能跑通,但换到 Apple M 系列或 AWS Graviton 实例就会暴露问题
- 调试时可用
std::atomic_thread_fence临时插桩,但上线必须回归到原子操作本身的 memory_order
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











