Skip to content

模型Model

读改写原语

施工中 · 内容逐步修订

Read-modify-write operation · RMW

将读取旧状态、依据旧状态决定更新及返回结果合为一个原子状态转移的同步原语;其内存序和进度保证需另行规定。

形式陈述 ​

一个原子状态转移 ​

对保存状态 x 的并发对象,RMW 的顺序规格可以写成

(x,a)⟼(Fa(x),Ra(x)),

其中 a 是调用参数,Fa(x) 是新状态,Ra(x) 是返回值。常见形式返回旧值,也有接口返回新值或成功标志,不能从名字猜定。

当多个调用并发发生时,线性化要求每个完成的调用,都能解释为在其调用与返回之间的某个瞬间执行了这个状态转移,并与其他调用形成符合规格的顺序。实现可能依赖专用指令、缓存一致性或重试;“原子”描述可观察语义,不要求硬件只用一个时钟周期。[1]

原语 新状态 常见返回值
exchange(v) v 旧值
fetch_add(d) 旧值 +d 旧值
test_and_set() true 旧布尔值
compare_exchange(e, v) 旧值等于 e 时改为 v,否则不变 成功标志,并可回传实际读到的值

对于最初的计数器,两个 fetch_add(1) 必须表现为某个先后顺序:一个读改写把 0 变成 1 并返回 0,另一个把 1 变成 2 并返回 1。因此可以把返回的旧值用作不重复的票号;这个论断假设计数器在所考察的范围内没有回绕。

直觉

两次正确的读取,也可能丢掉一次更新 ​

共享计数器初值为 0,两个线程都执行“读出、加一、写回”。即使单次读、写各自都是原子的,也可能出现:

时刻 线程 A 线程 B 共享值
1 读到 0 0
2 读到 0 0
3 写入 1 1
4 写入 1 1

两次增加最终只留下 1。问题不是某个整数被“读了一半”,而是读取旧值到提交新值之间,另一个更新插了进来。读改写原语(read-modify-write,RMW)把这一整段状态变换做成一次不可分割的操作。

例子与边界

CAS 怎样把计算与提交分开 ​

compare-and-swap(CAS)的思路是:“只有状态仍等于我据以计算的旧值,才提交新值。”对共享值做一般的变换 f 时,常见模式为:先读 a,在本地计算 b=f(a),再尝试把 a 换成 b;若比较失败,就读取新状态并重新计算。

例如,共享值从 10 开始。A 准备把它翻倍为 20,B 先增加到 11;A 随后的 CAS(10,20) 失败,所以不会用过期结果覆盖 B 的更新。A 改用 11 重新计算,再尝试提交 22。在这个成功次序下,结果等价于先加一、再翻倍。

失败重试意味着本地计算可能执行多次。因而不能把不可重复的外部副作用随意放进计算 f(a) 的部分;“成功 CAS 只发生一次”并不让之前的所有代码也只运行一次。

语言接口还可能区分弱 CAS 与强 CAS。以 C++ 的 compare_exchange_weak 为例,它允许比较值相同却报告失败,即伪失败;失败时还会用实际读值更新 expected。循环必须按该语义组织,不能把每次弱 CAS 失败都解释成另一个线程已经成功修改了对象。C++ 还把成功的 compare-exchange 归为 RMW,失败的比较归为原子读;两条路径的内存序约束并不相同。[2]

ABA 与进度保证 ​

CAS 比较的是当前值,不是完整修改历史。如果一个位置先为 A,中途变成 B,后来又变回 A,旧的比较仍可能成功。这就是 ABA 的基本形态。单纯的数值翻倍若只依赖当前值,未必因此出错;无锁链表若把相同地址误认为同一个未变的节点,就可能使用已经失效的结构信息。

给状态附加版本号,可以把“值相同但版本不同”区分开;还需要处理版本回绕。内存回收协议则控制节点何时能释放和复用,解决悬空引用以及部分地址复用问题,但不自动消除所有逻辑上的 ABA。

进度也不能从“用了 CAS”直接推出。无锁要求系统整体持续有操作完成,无等待要求每个操作都在有限个自身步骤内完成。某个线程可能在 CAS 循环中反复失败;阻塞分配器也可能破坏整体进度保证。要证明进度,必须分析整个实现及所依赖原语,而不是只看最后一条指令。

Herlihy 的共识层级还区分了原语的理论能力:在标准异步共享内存模型下,普通读写寄存器的共识数为 1;test-and-set、swap、fetch-and-add 的共识数为 2;CAS 的共识数为无限。这说明哪些无等待对象能够由哪些原语构造,不是在给硬件吞吐量或延迟排名。[1]

推论与应用

原子更新与内存可见性各自解决什么 ​

RMW 可以防止对同一原子对象的丢失更新,但其他内存位置的读写顺序仍由内存模型规定。以 C++ 为例,memory_order_relaxed 的 fetch_add 依然是原子 RMW,却不凭这次操作建立一般的跨线程数据发布关系。[2]

例如,一个线程先填写普通数据,再设置“已准备好”标志;另一个线程观察标志后读取数据。除了标志本身原子,还需要恰当的 release/acquire 同步,使前面的数据写入对后面的读取建立所需的顺序。把一个 relaxed 计数器放到旁边,并不能替代这项关系。

同样,对两个不同变量各做一次原子操作,也不自动形成“同时修改两个变量”的事务。单对象原子性、跨对象不变量与内存顺序,是三份需要分别说明的保证。

参考资料

[1] Maurice Herlihy,Wait-Free Synchronization,ACM TOPLAS 13(1),1991,124–149,§3:读改写操作与共识层级。

[2] C++ working draft,Order and consistency 与 Atomic types:原子 RMW、内存序和 compare-exchange 接口。

关系图谱13 个相邻概念 · 3 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
分类位置

上位 / 更一般

下位 / 直接特例

类型化关系