无锁编程里的幽灵:ABA 问题

假设你在写一个无锁栈,核心逻辑简单到让人心安:读栈顶指针 A,新建节点指向 A,用 CAS 把栈顶从 A 换成新节点。看起来天衣无缝——CAS 会帮你检查"栈顶还是不是 A",不是就重试,完美。但线程 1 刚读完 A 被挂起的那一小段窗口里,线程 2 可以弹出 A、弹出 A 的下一个,又把 A 重新压回去。栈顶值没变,还是 A,CAS 成功——可栈里面的数据关系已经全变了。你拿着一个"看起来没改"的指针,接上了一段早就被拆散的链表。这就是 ABA,无锁编程里最阴险的问题:CAS 只比较值,不比较"这个值是不是同一次出现"。

解决办法的核心思路都是让 CAS 能区分"同一个地址的两次不同出现"。最经典的是加版本号——Hazptr 论文里是用一个带 epoch 计数的指针包装,每次修改都递增版本,这样哪怕地址回到 A,版本号也变了,CAS 自然失败。Linux 内核里用的 atomic64_t 做 64 位 CAS 也就是这个思路:低 32 位放指针,高位放计数器,一次操作同时比较两者。另一个思路是 Hazard Pointer——线程在访问节点前先"挂号"声明自己正在用这个节点,回收器看到有人挂号就推迟释放,从根源上杜绝了"正好被重新分配回来"的可能。

ABA 的教训不只是并发技巧,它揭示了一个更本质的东西:相等 ≠ 同一。CAS 比较的是比特模式,不是对象身份,而并发场景下这两者经常不是一回事。 这就是为什么 64 位原子操作在内核里不是奢侈品而是刚需。

#CS