故事

光绪年间,保定府有家铁匠铺,专给镖局打马掌。铺子里三个伙计,老周掌钳,小周抡锤,还有个学徒阿贵负责淬火。一匹马四只蹄,一套马掌十二枚钉子,规矩是"一匹马、一炉铁、一柄钳"——老周的钳子夹哪块铁,这块铁就是这匹马的,旁人不得碰。

这年直隶闹马贼,镖局订单翻了三倍。老周一个人掌钳忙不过来,掌柜的想了个法子:三套炉子同时开,三个伙计各掌一钳,谁抢到活谁干。

头一天就出了乱子。两匹马的订单同时送到,小周和阿贵都瞅见炉台上剩最后一块好铁。小周伸手去拿,阿贵也伸手,两人各拽住一头,铁块在半空里晃悠。老周过来一瞧,这块铁已经被炉温烤得发软,两头各留了指印,中间拉出一道细颈——废了。

更糟的是后面。两人赌气,各自抓了块次等铁去钉马掌。小周那套钉完,镖师牵马出门;阿贵那套也钉完,另一镖师牵马出门。账房先生记账时傻了:两匹马,三套马掌,库存里却只剩一套钉子的料——有人多领了一份,有人重复记了账。

掌柜的连夜改规矩。他在炉台正中央挂了一块铜牌,牌上刻着"闲"和"忙"两个字,指针能转。规矩变成这样:

取铁之前,先看铜牌。指针指"闲",你才能伸手;伸手的同时,把指针拨到"忙"。从这一刻起,这块铁归你,旁人看见"忙"字,知道有人占了,便去干别的。等你钉完、交货、回炉台,再把指针拨回"闲"。

若两人同时伸手呢?那也得有个先后。手指碰指针的刹那,先碰到的那个把针拨过去,后碰到的那个发现针已经变了,知道自己晚了一步,收手去寻别的活计——不许硬抢,不许等对方干完,只许看一眼、拨一下、立刻决断

这规矩行了三十年,铺子后来扩成七家分号,炉台变成流水转盘,铜牌变成柜台上方一盏汽灯——绿灯闲、红灯忙。学徒阿贵成了大掌柜,他说:"不是铁有多金贵,是'正在被人改'这东西不能两个人同时信。你得先看见它没被人改,然后立刻宣布你正在改它——这两下子必须连成一气,中间不能插进任何人。"

概念解析

CAS(Compare-and-Swap)是硬件层面最古老的原子原语之一,1960 年代随 IBM System/370 的"比较并交换"指令进入计算机体系结构。它的语义极其朴素,却支撑了几乎所有上层并发控制——锁、无锁数据结构、事务内存、分布式乐观并发,追根溯源都能落回这一指令。

原子性的真正含义。不是"操作很快",而是"操作不可拆分"。读旧值、比较、写新值,这三步在 CAS 里被硬件保证为一个不可分割的整体——总线锁或缓存一致性协议把中间状态屏蔽掉,任何其他核心在这几纳秒里插不进来。

ABA 问题。铁匠铺的铜牌只有"闲""忙"两态,若某人把针拨到"忙"、干完活、拨回"闲",另一人再来,看到的仍是"闲",不知道这中间已经转过一圈。这在程序里表现为:指针从 A 变成 B 又变回 A,CAS 比较时以为没变化,实际上对象已被偷换。解决方法是加版本号——"闲-第 7 次"与"闲-第 8 次",在 CAS 眼里截然不同。

与锁的区别。锁是悲观的:先占住资源,再干活,相信冲突必然发生。CAS 是乐观的:先干活,提交时检查有没有冲突,没有就成功,有就重试。锁把并行串行化;CAS 让并行保持并行,只在最后那一下碰撞。在核数少的年代锁够用;核数上百时,锁的上下文切换和缓存失效代价超过 CAS 的重试代价,无锁结构才显出优势。

从单核到分布式。单核 CAS 靠总线锁;多核靠缓存一致性(MESI);分布式系统里没有共享总线,于是把 CAS 语义推广为分布式 CAS——etcd 的 ModRevision、ZooKeeper 的 version、DynamoDB 的 ConditionalWrite,本质都是"先读一个版本号,写入时带上这个版本号,服务端比较匹配才生效"。网络延迟把纳秒级的原子操作拉长到毫秒,但语义没变:看见、宣布、连成一气。

现代形态。Java 的 AtomicInteger.compareAndSet、C++ 的 std::atomic::compare_exchange_strong、Go 的 atomic.CompareAndSwapInt64,封装了不同架构的底层指令(x86 的 CMPXCHG、ARM 的 LDXR/STXR)。再高一层,MVCC([待写])用版本链实现读写不阻塞,其提交阶段的冲突检测正是 CAS 的逻辑扩展;Redis 的 WATCH/MULTI/EXEC 事务,也是 CAS 在键值层面的应用。

铁匠铺后来用的汽灯,绿灯红灯之间没有渐变——要么闲,要么忙。这种二元决断是 CAS 的气质:不仲裁、不排队、不协商,只在一瞬间判定"这仍是我以为的那个世界吗",然后行动或放弃。所有更复杂的协调,都是这个瞬间的递归组合。