4-5.【OC】【锁】CAS 为什么会产生 ABA 问题?在 iOS 中如何规避?

3 阅读3分钟

CAS(Compare-And-Swap)虽然是无锁编程的基石,但它有一个致命的逻辑漏洞:它只检查“值”是否相等,而不检查“值”是否被改动过。


1. 为什么会产生 ABA 问题?

CAS 的逻辑是: “如果当前值等于我预期的旧值,我就更新它。”

但由于 CPU 调度,可能会发生以下情况:

  1. 线程 1 准备将变量从 A 改为 X。它读取了当前值 A,然后被挂起。
  2. 线程 2 介入,将变量从 A 改成了 B
  3. 线程 2(或线程 3)又将变量从 B 改回了 A
  4. 线程 1 恢复运行,执行 CAS 操作。它发现当前值依然是 A,认为一切如常,更新成功。

为什么这很危险?

如果 A 仅仅是一个数值(如 100),ABA 可能没问题。但如果 A 是一个内存指针,问题就大了。

经典案例(栈的弹出):

线程 1 准备弹出栈顶元素 A,它记录了 A.nextB。在执行 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_lockDispatchQueue (Serial)
  • 理由: 锁在语义上自带“互斥”,它天然地阻止了 ABA 的发生,且现代 os_unfair_lock 的性能极其接近原子操作,只有在极高频竞争下才有必要考虑无锁化。

总结比较

方案优点缺点
CAS (裸)极快、无死锁存在 ABA 风险、实现复杂
版本号 (Tagged Pointer)彻底解决 ABA占用更多空间、非原生支持
os_unfair_lockiOS 推荐、安全、高性能会导致线程挂起(非忙等)
Swift Actors语法级安全、无数据竞争异步开销、需要 Swift 5.5+