CAS(Compare-And-Swap)虽然是无锁编程的基石,但它有一个致命的逻辑漏洞:它只检查“值”是否相等,而不检查“值”是否被改动过。
1. 为什么会产生 ABA 问题?
CAS 的逻辑是: “如果当前值等于我预期的旧值,我就更新它。”
但由于 CPU 调度,可能会发生以下情况:
- 线程 1 准备将变量从
A改为X。它读取了当前值A,然后被挂起。 - 线程 2 介入,将变量从
A改成了B。 - 线程 2(或线程 3)又将变量从
B改回了A。 - 线程 1 恢复运行,执行 CAS 操作。它发现当前值依然是
A,认为一切如常,更新成功。
为什么这很危险?
如果 A 仅仅是一个数值(如 100),ABA 可能没问题。但如果 A 是一个内存指针,问题就大了。
经典案例(栈的弹出):
线程 1 准备弹出栈顶元素 A,它记录了
A.next是B。在执行 CAS 切换栈顶前,线程 2 弹出了 A 和 B,并随后又压回了 A(但此时 A 的next指向了全新的C)。线程 1 恢复后发现栈顶还是 A,执行 CAS。结果栈顶变为了
B——然而B已经被释放或挪作他用了,程序瞬间崩溃。
2. 在 iOS 中如何规避?
在 iOS 开发(Objective-C 或 Swift)中,我们通常有三种层面的方案:
A. 使用带有版本号/时间戳的包装
这是最通用的解法:不仅对比“值”,还对比“修改版本”。
- 原理: 每次修改变量时,同步递增一个版本号。CAS 同时检查
(Value, Version)这一对组合。 - 实现: 即使值变回了 A,版本号也已经从
1变成了3,CAS 就会失败。
B. 利用内存屏障与高级同步原语
现代 iOS 开发中,我们很少直接写原始的 CAS 指令。
- Swift Atomic (Swift 6+): Swift 官方推出的
Synchronization库提供了安全的原子操作。 - OSAtomic (已弃用) -> atomic_compare_exchange: 在 C/Obj-C 层面,使用
stdatomic.h里的强类型原子函数。虽然它们底层也是 CAS,但配合正确的内存模型(Memory Order)可以减少逻辑错误的概率。
C. 避免裸指针操作(ARC 的隐形保护)
在 iOS 的 ARC(自动引用计数)环境下,只要你持有一个对象的强引用,它的内存地址就不会被轻易回收再分配。
- 引用计数逻辑: 只要线程 1 还在处理指针 A,A 就不会被销毁。这在一定程度上缓解了指针重用导致的 ABA 崩溃,但逻辑上的 ABA(状态回退)依然存在。
D. 终极方案:使用锁或串行队列
如果并发逻辑复杂到需要担心 ABA 问题,通常意味着无锁编程的复杂度已经超出了维护成本。
- 建议: 优先使用
os_unfair_lock或 DispatchQueue (Serial) 。 - 理由: 锁在语义上自带“互斥”,它天然地阻止了 ABA 的发生,且现代
os_unfair_lock的性能极其接近原子操作,只有在极高频竞争下才有必要考虑无锁化。
总结比较
| 方案 | 优点 | 缺点 |
|---|---|---|
| CAS (裸) | 极快、无死锁 | 存在 ABA 风险、实现复杂 |
| 版本号 (Tagged Pointer) | 彻底解决 ABA | 占用更多空间、非原生支持 |
| os_unfair_lock | iOS 推荐、安全、高性能 | 会导致线程挂起(非忙等) |
| Swift Actors | 语法级安全、无数据竞争 | 异步开销、需要 Swift 5.5+ |