“所有共享指针与hazard槽读写都是SC 原子操作。移除节点后先retire,放入退休列表;每次回收先固定一批已经退休的候选节点,再扫描全部hazard槽,只释放该批中未被任何槽命中的节点。…”
形式陈述
设共享对象由一台抽象状态机描述。称对象上的一个操作是原子的,若在抽象层面的并发对象历史中,它把对象从一个状态不可分割地转移到另一个状态:任何其他线程都观察不到它的中间状态。等价的表述是,每个原子操作可以被赋予一个位于其调用与返回之间的单一时刻(线性化点),仿佛整个效果在该瞬间一次性发生;操作之间的可见效果与按这些时刻排成的顺序执行一致。原子性是对"抽象历史中的粒度"的规定,与实现层的步数无关:一个操作可以由多条机器指令实现,只要并发观察者无法看穿其内部即可。
直觉
把高级操作当成一步,意味着客户端不能利用它的内部交错获得顺序规格禁止的结果。实现可以先分配节点、再修改指针;需要证明的是这些内部动作产生的所有调用—返回历史,都能解释为合法原子步骤。线性化点有时取成功的 CAS,有时依赖另一线程的帮助动作,未必是调用者固定的一条指令。
原子性必须标明对象边界。一个队列的 enqueue 可以原子,却不能推出“向两个队列各入队一次”也是原子操作;两个独立对象之间仍存在其他调用可插入的间隔。它也不保证等待时间:把整个方法置于锁内可以满足原子规格,但持锁者停止时其他调用可能永远等待。
例子与边界
正例:线性一致的原子寄存器不仅禁止撕裂,还要求读值能放入保留实时先后的合法顺序。初值为 0,write(1) 已返回后才开始的读,在没有其他写时必须返回 1;仅承诺“读到某次完整写的值”仍可能允许返回过时的 0,所以无撕裂只是必要的一部分。反面对照能看清它防住了什么:在只支持 32 位原子访问的机器上用两条指令写一个 64 位数,并发读者可能读到"前一半旧值、后一半新值"拼成的从未被写入过的数——原子性排除的正是这种中间状态泄露。
边界情形之一是复合操作:先原子地读
例如库存为 1,A、B 先后原子读取都得到 1,随后各自写入 0 并发货,库存字段始终完整却发出了两件商品。用 compare_exchange(1,0) 把检查与扣减合为一次 RMW 后,只有一个调用能成功,另一个必须按失败结果处理。若还需同时写订单记录,单个库存 CAS 并未覆盖该跨对象不变量,仍要另定事务或同步边界。
推论与应用
原子操作是同步对象的实现基础:互斥锁可用原子测试与更新转移所有权,信号量实现则需原子维护许可计数及等待队列状态,引用计数依赖原子增减,无锁数据结构以比较并交换为骨架并追求lock-free 进展。这些对象各自还规定所有权、阻塞或唤醒语义,不能从“底层更新是原子的”直接推出完整接口正确性。
在正确性理论中,为每个操作指定线性化点是证明线性一致性的标准手法,原子操作因此连接实现与顺序规格。不同原子原语的表达力并不相同;它们能为多少参与者实现何种对象,由共识数层级统一刻画。
参考资料
- Maurice Herlihy and Nir Shavit, The Art of Multiprocessor Programming, rev. 1st ed., Morgan Kaufmann, 2012,Chs. 1–18。
- Hagit Attiya and Jennifer Welch, Distributed Computing: Fundamentals, Simulations, and Advanced Topics, 2nd ed., Wiley, 2004,Chs. 1–18。