不可分割是一种语义保证

并发程序关心的原子性,不是操作在物理上没有内部步骤,而是其他相关操作不能观察或插入一个不被该接口允许的中间状态。以同一原子对象为例,原子访问之间需要满足语言规定的不可分割性;这是一种可观察行为的约束,不是对执行耗时的承诺。

因此,“一行代码”“一条看起来简单的赋值”和“一个原子操作”并不等价。应依据语言接口、对象类型和平台保证判断,而不能仅从源码长度推断。原子操作也不等于整个函数不可被调度器打断。

参考:[2] [4]

读改写为何不同

原子读取取得一个值,原子写入替换一个值;把这两步分开,中间仍可发生其他修改。原子读改写则把读取旧状态与相应更新结合为一个不可分割的操作。C 的 atomic_fetch_add 等接口在更新的同时返回旧值,适合表达共享计数器的一次增量。

比较并交换则把对象值与预期值比较,相等时才写入新值,并报告成功与否。它常用于“基于观察到的旧状态尝试更新”的循环。C++ 接口还区分弱比较并交换与强比较并交换,调用者不能把失败一概解释成业务条件永久不满足。

C++的整数比较并交换里,expected也不只是一个固定输入:实际值不符而比较失败时,接口会把观察到的值写回expected,供下一轮计算desired。弱版本还允许伪失败,即数值表示相同也可返回false;因此常用循环重新计算候选,而不能仅凭一次false断定另一个线程已经改过数据。强弱版本与成功、失败的内存顺序分别属于接口约定,把它们混成一次普通真假判断会遗漏这些状态变化。

参考:[1] [3] [5]

原子性之外还有内存顺序

原子性约束单个操作,内存顺序进一步约束不同操作在其他线程中的可见关系。C 与 C++ 都提供 relaxed、acquire、release 及顺序一致等形式。宽松原子访问仍具有相应的不可分割性,但不能单凭它推断其他普通数据已经被安全发布。

在 C++ 规定的条件下,针对同一原子对象的获取操作若读取了相应释放序列的值,就能形成同步关系。顺序一致操作还受到统一全序约束,但这并不把程序中的任意多个操作打包为一个事务。选择较强顺序可以改变允许的观察结果,却不能代替对算法步骤的设计。

参考:[2] [1]

构件不是完整算法

例如两个线程分别对原子计数器执行“读取、计算加一、写回”,两者仍可能读到相同旧值并写回相同新值。这里每次访问都可以是原子的,整个加一却不是一次原子读改写。使用适当的读改写接口或对完整步骤加锁,才能表达所需的更新约束。

同样,原子接口与无锁实现不是同一概念。C++ 提供查询某种原子类型操作是否无锁的接口,说明不能仅凭名称假定实现方式。多个对象之间的一致性、对象初始化完成后再使用、竞争下的进展等问题,仍需独立分析;原子性只是正确并发协议的一部分。

参考:[3] [4]