Skip to content

并发算法与理论

Posted on:2022.09.06

TOC

Open TOC

Info

https://hongjin-liang.github.io/teaching/concurrency/index.html

Overview

Concurrent Program

A concurrent program = concurrent objects + their clients

concurrent objects 可以是一个 mutex,也可以是一个并发数据结构

client 只能通过调用 concurrent objects 的接口访问共享内存

多个 client 会并发的访问 concurrent objects,因而会产生不确定的交错访问

Memory Models

下面介绍内存模型,若硬件能够提供源代码级别程序的交错访问,那么被称为 Sequential Consistency

interleaving semantics

然而

每一种硬件架构都会提供自己的弱内存模型,例如

3287d03f2a694298bb654cdb9aa4ae4e.png

ce9dae012cb945ba8df77e820c8ddfe6.png

这就会带来一些问题,例如 Store buffering (SB)

考虑如下一段程序

Initially: x = y = 0
// thread 1
x = 1;
r1 = y;
// thread 2
y = 1;
r2 = x;

在 x86-TSO memory model 下,可能会出现 r1 = r2 = 0,也就是数据只写入了 buffer,还未写入共享内存,这种情形在 SC 下是不可能发生的

在编译优化下,还可能出现指令乱序,例如 thread 1 的 r1 = y;x = 1; 之前执行,同样也会导致 r1 = r2 = 0

另一个例子是 Load buffering (LB)

Initially: x = y = 0
// thread 1
r1 = x;
y = 1;
// thread 2
r2 = y;
x = 1;

可能会出现 r1 = r2 = 1,也就是读操作被延迟了

在这样的环境下,高级语言的程序员需要考虑硬件架构,又要考虑编译优化,实在是太困难了

于是,硬件设计者和编译器设计者约定,只要程序具有 DRF 的性质,那么就可以保证程序的执行是 SC 的,所以对于 client 端,并不需要考虑内存模型

然而,concurrent objects 端为了提高性能,在实现 concurrent algorithms 时,可能并不需要 SC,所以需要考虑内存模型

Course Overview

Memory Models

Design Criteria

  1. DRF programs have the same behaviors as in SC model

需要定义何为 DRF - A data race occurs when we have two concurrent conflicting operations

  1. Not too strong

要支持主流的编译优化

要支持主流硬件架构的 compilation schemes

  1. Not too weak

保证类型安全和一些安全性保证

Happens-Before Memory Model (HMM)

4b8b7edc377744daa4afcacc4541b692.png

上面提过,Happens-Before Order 就是 program-order 和 synchronizes-with 并集的传递闭包

在这样的内存模型下,read 可见的 write 如下

  1. the most recent write that happens-before it
  2. a write that has no happens-before relation

例如 r 读取的值可能是 w1 写入的,也可能是 w2 写入的

good speculation

对于之前的例子

Initially: x = y = 0
// thread 1
x = 1;
r1 = y;
// thread 2
y = 1;
r2 = x;

是否可能出现 r1 = r2 = 0,根据 Happens-Before Order 建模

flowchart LR A["x = 0, y = 0"] --> B["x = 1"] A --> C["y = 1"] B --> D["r1 = y(0) - speculation"] C --> E["r2 = x(0) - speculation"] A -.-> D A -.-> E

虚线代表了 read 可能读取的 write 的来源,都是 no happens-before,所以是可能的

bad speculation

但是,在 HMM 下,可能出现 Out-of-Thin-Air Read,即凭空的 read

考虑如下的例子

Initially: x = y = 0
// thread 1
r1 = x;
y = r1;
// thread 2
r2 = y;
x = r2;

是否可能出现 r1 = r2 = 42,根据 Happens-Before Order 建模

flowchart LR A["x = 0, y = 0"] --> B["r1 = x(42) - speculation"] A --> C["r2 = y(42) - speculation"] B --> D["y = r1(42) - accord to po"] C --> E["x = r2(42) - accord to po"] E -.-> B D -.-> C

注意这里是先对结果进行 speculation,然后根据 Happens-Before Order 得到其他的值,最终判断结果值的来源

虚线代表了 read 可能读取的 write 的来源,都是 no happens-before,所以是可能的

这就有可能破坏安全性保证

JMM

以 HMM 为核心,试图区分 good speculation 和 bad speculation,却引入了更多的问题

http://www.cs.umd.edu/~pugh/java/memoryModel/

Operational Semantics

Basic domains

r ∈ Reg
x ∈ Loc
v ∈ Val
i ∈ Tid = {1, ... ,N}

Expressions and commands

e ::= r | v | e + e | ...
c ::= skip | if e then c else c | while e do c | c ; c | r := e | r := x | x := e | r := FAA(x, e) | r := CAS(x, e, e) | fence

这里区分了 x 和 e,对 x 的读写需要访问内存

Programs

P : Tid → Cmd

written as

P = c1 ∥ ... ∥ cN

The thread subsystem

Store

s : Reg → Val

s 代表寄存器的集合

State

⟨c, s⟩ ∈ Command × Store

Transitions

 skip; c,sεc,s\overline{\text { skip; } c, s \stackrel{\varepsilon}{\rightarrow} c, s}

skip 指令直接跳过,此处 ε\varepsilon 表示无需与内存子系统进行交互

c1,sIc1,sc1;c2,sIc1;c2,s\frac{c_1, s \stackrel{I}{\rightarrow} c_1^{\prime}, s^{\prime}}{c_1 ; c_2, s \stackrel{I}{\rightarrow} c_1^{\prime} ; c_2, s^{\prime}}

定义 ; 的语义,此处 II 代表线程子系统和内存子系统的一次交互,例如 R,W,U,FR,W,U,F

s=s[rs(e)]r:=e,sε skip, s\frac{s^{\prime}=s[r \mapsto s(e)]}{r:=e, s \stackrel{\varepsilon}{\rightarrow} \text { skip, } s^{\prime}}

此处 s[rs(e)]s[r \mapsto s(e)] 表示单点修改寄存器 rr 的值,s(e)s(e) 表示通过寄存器计算 ee 的值

I=R(x,v)r:=x,sIskip,s[rv]\frac{I=R(x, v)}{r:=x, s \stackrel{I}{\rightarrow} \operatorname{skip}, s[r \mapsto v]}

通过内存子系统读取内存 xx 的值到变量 vv 中,并修改寄存器 rr 的值

I=W(x,s(e))x:=e,sI skip, s\frac{I=W(x, s(e))}{x:=e, s \stackrel{I}{\rightarrow} \text { skip, } s}

通过内存子系统将 ee 的值写入内存 xx

s(e)0if e then c1 else c2,sεc1,ss(e)=0if e then c1 else c2,sεc2,s while e do c,sε if e then (c; while e do c) else skip, s\begin{array}{l} \frac{s(e) \neq 0}{\text{if } e \text{ then } c_1 \text{ else } c_2, s \stackrel{\varepsilon}{\rightarrow} c_1, s} \newline \frac{s(e) = 0}{\text{if } e \text{ then } c_1 \text{ else } c_2, s \stackrel{\varepsilon}{\rightarrow} c_2, s} \newline \frac{}{\text { while } e \text { do } c, s \stackrel{\varepsilon}{\rightarrow} \text { if } e \text { then }(c ; \text { while } e \text { do } c) \text { else skip, } s} \end{array}

分支和循环语句,将循环递归转换为分支

I=U(x,v,v+s(e))r:=FAA(x,e),sI skip, s[rv]\frac{I=U(x, v, v+s(e))}{r:=\mathbf{FAA}(x, e), s \stackrel{I}{\rightarrow} \text { skip, } s[r \mapsto v]}

Fetch-and-add,语义是将读取内存 xx 的值到寄存器 rr 中,并增加内存 xx 的值,此处引入了新的记号 UU,代表原子读写内存

I=R(x,v)vs(er)r:=CAS(x,er,ew),sI skip, s[r0]I=U(x,v,s(ew))v=s(er)r:=CAS(x,er,ew),sI skip, s[r1]\begin{array}{l} \frac{I=R(x, v) \quad v \neq s(e_r)}{r:=\mathbf{CAS}\left(x, e_r, e_w\right), s \stackrel{I}{\rightarrow} \text { skip, } s[r \mapsto 0]} \newline \frac{I=U(x, v, s(e_w)) \quad v = s(e_r)}{r:=\mathbf{CAS}\left(x, e_r, e_w\right), s \stackrel{I}{\rightarrow} \text { skip, } s[r \mapsto 1]} \end{array}

Compare-and-swap,语义是将读取内存 xx 的值到变量 vv 中,若值和 ere_r 不等,则置寄存器 rr00,否则置寄存器 rr11,同时修改内存 xx 的值

注意这里两个对 storage subsystem 的交互是不同的,在后面 TSO storage subsystem 中会有所体现

 fence, sF skip, s\frac{}{\text { fence, } s \stackrel{F}{\rightarrow} \text { skip, } s}

最后是 fence 指令,使用 FF 强制内存读写

参考 The Art of Multiprocessor Programming Chapter 5,若不存在 FAA 或 CAS 指令,某些 current objects 是无法实现的

SC storage subsystem

对内存的读写对所有的线程即时可见

Machine state

M : Loc → Val

Transitions

I=W(x,v)Mi:IM[xv]\frac{I=W(x, v)}{M \stackrel{i: I}{\longrightarrow} M[x \mapsto v]}

此处 i:Ii:I 表示线程 ii 对内存的修改

I=R(x,v)M(x)=vMi:IM\frac{I=R(x, v) \quad M(x)=v}{M \stackrel{i: I}{\rightarrow} M}

读不修改内存

I=U(x,vr,vw)M(x)=vrMi:IM[xvw]\frac{I=U\left(x, v_r, v_w\right) \quad M(x)=v_r}{M \stackrel{i: I}{\rightarrow} M\left[x \mapsto v_w\right]}

读内存并且修改

I=FMi:IM\frac{I=F}{M \stackrel{i: I}{\rightarrow} M}

fence 指令所带来的操作是 no-op

Lifting to concurrent programs

State

⟨P, S⟩ ∈ (Tid → Cmd) × (Tid → Store)

每个 tid 对应一个 cmd 和 store

Transition

P(i),S(i)Ic,sP,Si:IP[ic],S[is]\frac{P(i), S(i) \stackrel{I}{\rightarrow} c, s}{P, S \stackrel{i: I}{\longrightarrow} P[i \mapsto c], S[i \mapsto s]}

实际上定义了每个 thread 对 cmd 和 store 的修改在全局视角下的表示

Linking the thread and storage subsystems

综合 P,S,MP,S,M

Transition

不访问内存的情形下,有

P,Si:εP,SP,S,MP,S,M\frac{P, S \stackrel{i: \varepsilon}{\longrightarrow} P^{\prime}, S^{\prime}}{P, S, M \Rightarrow P^{\prime}, S^{\prime}, M}

访问内存的情形下,有

P,Si:IP,SMi:IMP,S,MP,S,M\frac{P, S \stackrel{i: I}{\rightarrow} P^{\prime}, S^{\prime} \quad M \stackrel{i: I}{\rightarrow} M^{\prime}}{P, S, M \Rightarrow P^{\prime}, S^{\prime}, M^{\prime}}

Allowed outcome

An outcome is a function

O : Tid → Store

An outcome O is allowed for a program P under SC if there exists M such that

P, S0, M0 =>∗ skip ∥ ... ∥ skip, O, M

定义了在 SC 内存模型下,所有线程寄存器的状态的可能结果

TSO storage subsystem

每个 thread 有一个 store buffer

所以内存的 Machine state 需要增加

A function B : Tid → (Loc × Val)∗

例如

thread #1 -> ((x, 1), (y, 2), (x, 3))

注意 store buffer 中的顺序是关键的

由此可以定义如下 transitions

e20d164691a6472284977ecfa07afde5.png

PSO storage subsystem

279d23d153bb41a7967e1c63f6ad1ef1.png

PSO 内存系统比 TSO 更弱,体现在 store buffer 对于不同变量的写可以乱序,例如,对于如下写操作

x = 1
y = 2
x = 3

store buffer 可以为

(x, 1), (x, 3), (y, 2)

也可以为

(y, 2), (x, 1), (x, 3)

为此,需要修改 write transition

I=W(x,v)B(i)=b1b2xb1M,Bi:IM,B[ib1x,vb2]\frac{I=W(x, v) \quad B(i) = b_1 \cdot b_2 \quad x \notin b_1}{M, B \stackrel{i: I}{\longrightarrow} M, B[i \mapsto b_1 \cdot \langle x, v\rangle \cdot b_2]}

添加 store-store-fence 交互

I=SSFM,Bi:IM,B[iSSFB(i)]\frac{I = SSF}{M, B \stackrel{i: I}{\longrightarrow} M, B[i \mapsto \text{SSF} \cdot B(i)]}

实际上是在 store buffer 中添加一个标签 SSF\text{SSF}

略微修改 write transition 即可

I=W(x,v)B(i)=b1b2xb1SSFb1M,Bi:IM,B[ib1x,vb2]\frac{I=W(x, v) \quad B(i) = b_1 \cdot b_2 \quad x \notin b_1 \quad \text{SSF} \notin b_1}{M, B \stackrel{i: I}{\longrightarrow} M, B[i \mapsto b_1 \cdot \langle x, v\rangle \cdot b_2]}

Declarative Semantics

换种方式写笔记

1aabad23ed0f4f2fb05875a0e964e686.png

18f3c85f5b494853a706d2ec205e2568.png

这种定义语义的思想在之前 HMM 中已经有所体现,这一节将其形式化

Executions

4c2fbdc2f213447cba27f7f531c7fc9c.png

Events 是图中的顶点,Relations 则是图中的边

rf 感觉就是对同一个内存地址所读取的值的 speculation

265b9e1bd649422993ae114bab3c4701.png

此处形式化定义 Event,i ∈ Tid ∪ {0} 中并上 0 是为了表示程序的初始状态,例如 x = 0, y = 0

032216ccad6f4c8e85723b4b9e6bf696.png

此处形式化定义 Execution graph,顶点就是上述定义的 Events 的有限集,边目前包括下述两种

flowchart LR A["W x 0"] --> B["R x"] C["W x 1"] --> B

4f50d5f9aadd4a85a4caaa55201cf8d0.png

这里定义了一些记号,主要是为了描述某种类型的顶点或边

Mapping programs to executions

01fed6eeaea74e4e826ce88b48dfa8d0.png

dd4a7786947a4714b58de298f64ebc8b.png

下面需要考虑将程序映射到 execution graph,目前只考虑 po

ea2e85e31f7b424e8568f85f1e3264df.png

这里利用上一节中定义的操作语义,形式化的描述了通过 command 构造 execution graph 的定义

举个例子

x = 1;
y = 1;
a = x; // 1

那么对应的 execution graph 为

flowchart LR A["<0, 0, (W x 1)>"]-->B["<1, 0, (W y 1)>"] B-->C["<2, 0, (R x 1)>"] A-->C

4264d5cc4f20476a81cfafbbc14cdfbb.png

对于给定的 thread,只需选取其中对应的 event,并修改其 tid 为 0,就能得到原图的一个子图

对于一个 graph,若对所有的 thread 执行上述操作,都能得到满足上述定义 (sequential) 的一个子图,则该 graph 即为源程序的一个 execution graph

Consistency predicate

5344f724977e47b3bc825ae85f88da1b.png

要求 execution graph 满足某些 consistency predicate

e2d5899b5f454111a750908227c022f3.png

例如,要求 rf 边的 codomain 能够取满 read event 的定义域

Sequential consistency

1d488dd58d074eccab805c8ec530a06d.png

7e1c2f1da8854b6f9f7e4e3f67093fcf.png

下面使用 declarative semantics 定义 SC

flowchart LR A["<0, 1, (W x 1)>"] --rf--> B["<2, 1, (R x 1)>"] A --sc--> C C["<1, 1, (W x 2)>"] --sc--> B

b4dcc9acec7f49f0927715f57ff2b2af.png

分析右侧的 execution graph 为何不是 SC

flowchart LR A["W x 0"] B["W y 0"] C["W x 1"] D["W y 1"] E["R y 0"] F["R x 0"] A --po--> D B --po--> C A --po--> C B --po--> D C --po--> E D --po--> F A --rf--> F B --rf--> E D --sc--> E

则违反上述定义,又因为 sc 边是全序关系,所以 W y 1R y 0 只能是如下关系,同理可得 W x 1R x 0

flowchart LR A["W x 0"] B["W y 0"] C["W x 1"] D["W y 1"] E["R y 0"] F["R x 0"] A --po--> D B --po--> C A --po--> C B --po--> D C --po--> E D --po--> F A --rf--> F B --rf--> E E --sc--> D F --sc--> C

构造其传递闭包,会发现由于出现了环,违反了全序关系的反对称性,所以不是 SC

a24bc4f171e845cb9304950b8a294f2f.png

还有一种等价的 SC 定义,引入 morb

注意这里 mo 边实际上构成了对于某个内存地址写的一个全序关系

rb 边定义中的 \id 的含义是去掉 identity 的边,避免 update event 形成自环构成 rb

02038e4c9f2a428f88906c8f4a585f3c.png

仍然是上述例子,构造其 morb

flowchart LR A["W x 0"] B["W y 0"] C["W x 1"] D["W y 1"] E["R y 0"] F["R x 0"] A --po--> D B --po--> C A --mo--> C B --mo--> D C --po--> E D --po--> F A --rf--> F B --rf--> E E --rb--> D F --rb--> C

同样形成环

d083d25d8d734acfa0d9e625779043b0.png

Relaxing sequential consistency

略微放松 SC 的语义,即在每个内存地址上实现 SC

f6b36b7328eb4e799175b81596e56421.png

这里定义的含义是

⟨a, b⟩ ∈ [RWx]; G.po; [RWx]

顶点 a 和顶点 b 是关于内存地址 x 的读或写的 event,且顶点 a 和 b 之间的边为 po

c2dab47c005143f796ab5c3fef480900.png

其等价的定义相当于取图中相同内存地址的 po 边,和 rf / mo / rb 边取并集

由于 rf 边和 mo 边的定义保证了其两个顶点一定是对同一个内存地址的 event,所以 rb 边的两个顶点也是对同一个内存地址的 event,这样四者的并集其中的顶点一定是对同一个内存地址的 event

下面人为定义一些 bad patterns,即违反了四者的并集无环的要求

8bd05bf0af4944fb9bb4990d8a21fc20.png

这两个 patterns 很明显

16961a4eeaf64cc8bd6835d9cb7b6a56.png

对于 coherence-wr 和 coherence-rr,考虑形成的 rb 边即可

d861fd0023004e1baef3f37840417a75.png

atomicity 同样考虑形成的 rb 边即可

9dcf10794fde4b0e88f930d93a42f816.png

综合上述情形,可以形成对 COH 的另一个等价定义,其证明略

下面来看几个例子

2503197d100b4421bbc2654703dae2da.png

第一个例子,首先画出 po 边和 rf

flowchart LR A["W x 0"] B["W x 1"] C["W x 2"] D["R x 2"] E["R x 1"] A --po--> B A --po--> C B --po--> D C --po--> E B --rf--> E C --rf--> D

由于 mo 边为全序关系,所以 W x 1W x 2 之间一定存在一条 mo 边,不妨如下

flowchart LR A["W x 0"] B["W x 1"] C["W x 2"] D["R x 2"] E["R x 1"] A --po--> B A --po--> C B --po--> D C --po--> E B --rf--> E C --rf--> D B --mo--> C E --rb--> C

那么 rb 边和 po 边之间就会形成环,所以在 COH 内存模型下是不被允许的

第二个例子,上面已经分析过了,由于仅考虑每个内存地址上的 SC,所以上述的长度为 4 的环也就不存在了,于是在 COH 内存模型下是被允许的

RA memory model

引子,使用 FAA 指令可以保证在 COH 内存模型下不会出现 a = 1b = 1

5c5c780d5ad74f319305e2f138c662cf.png

考虑使用 CAS 指令实现相同的语义

e206075304384450bf3917304a093190.png

对于同样的 store buffering 的例子,实际上在 COH 内存模型下,上述情形仍然是被允许的,其根本原因是即使存在 lock,对于一个内存地址的读仍然不能保证其之前的指令对于另一个内存地址的写及时可见

4a92ac6998fc4f99a9cf173cdd0f1c78.png

本质上这一种消息传递的编程范式,然而 COH 内存模型并不支持

于是考虑增强 COH 内存模型

1884e9af896d4a088ab52fe22115a0bc.png

即增强 coherence-wr 的能力,从而能够 forbid 更多不合理的情形

例如分析 slide #28 的第二个例子

flowchart LR A["W x 0"] B["W y 0"] C["W x 42"] D["W y 1"] E["R y 1"] F["R x 0"] A --po--> C A --po--> E B --po--> C B --po--> E C --po--> D E --po--> F D --rf--> E A --rf--> F A --mo--> C B --mo--> D F --rb--> C

注意这里 R x 0 -> W x 42 -> W y 1 -> R y 1 构成了环,所以在 RA 内存模型下是不被允许的

需要理解此处的 (po ∪ rf)+ 利用了对另一个内存地址的访问,考虑起点和终点,实际上是 <W x 42, R x 0> ∈ (po ∪ rf)+,仍然是对同一个内存地址的访问,所以没有问题

在 COH 内存模型下,由于缺少了 rf 边,上述闭包就无法构造,于是在 COH 内存模型下是被允许的

16fcda66c8f048af956d303694f8c827.png

这便是 RA 内存模型

f01c62eda414486fb89bdd38be382f01.png

存在其等价的定义

第一条等价于 po ∪ rf 无环

1ae7aa5a33c84643b58c099bfc84b330.png

Towards C/C++11 memory model

d37928f8a3524e129a771cdafeee52e4.png

考虑弱化 RA 内存模型

2940e0f68ab349d799526514877400d3.png

使用 access modes 注解每次内存访问

强于 acq 或 rel 的访问遵循 RA 内存模型,否则遵循 COH 内存模型

由此可以形式化的定义 Happens-Before 的语义,注意这里 synchronizes-with 是一个基本的定义,其中 表示不弱于,下一节会进一步扩充

007d1a70abbb45f99c08e9bf01b17920.png

由此可以定义 C11 内存模型

2ff45caa3e6143fd987be01b31ed4cf1.png

eedb1cd1ebc2460f839e92f9aa1aaf62.png

The C/C++11 memory model

5ceba6c1816a43df9bb1882321edc047.png

fe40751ddc95419187a3f08b1218b6d2.png

1453f80630a14cbf9657e1f8c6140d6b.png

较上一节中介绍的 C11 内存模型,这一节会

  1. 在存在 data race 的情形下,在 non-atomic 内存模型下程序的行为是未定义的 (catch-fire semantics)
  2. 四种类型的 fence
  3. 提供 SC 内存模型
  4. 更加精细的 synchronizes-with 定义

More elaborate definition of sw

fb6a17c3e1be481bba12dd57f4905977.png

首先是几个例子,对于前两个例子,程序的行为是未定义的,后两个例子则是有定义的

f724a17eddfc4cb895c232521a4ffc1c.png

需要在 synchronizes-with 的定义中考虑 fence,加下划线的是添加的部分,本质上就是在 write 前存在 fence,或者在 read 后存在 fence

b20d0bb069114b9e94b672e1994fe00e.png

考虑另外一种情形,此处 FAI 代表 fetch-and-increase,我们希望 rel 和 acq 之间的 update event 不会影响 synchronizes-with 关系的建立

于是只需将 rf 修改为 rf+,由于 rf 边要求是从 write event 指向 read event,所以中间的 event 只可能是 update event

5227453149cc44ddaa1aea3d1be52cc6.png

最后一种情形是,我们希望 rel 和 acq 之间对同一个内存地址的任意 access modes 的写不会影响 synchronizes-with 关系的建立,加下划线是修改的部分,其中 ? 代表可有可无

49688a5c6e454365a677b1450737132c.png

于是在 C11 中,最终的 synchronizes-with 关系定义如上

这里还列举了对 event 所有可能的 access modes,注意这里 na 代表 non-atomic,且 update event 不存在 non-atomic 注解,这与之前操作语义的定义是类似的

Catch-fire semantics

9ddfc2bfbc42482ab528415b064e762c.png

形式化定义何为 data race

  1. 对同一个内存地址,至少有一个是写操作
  2. 至少有一个操作的 access modes 为 non-atomic
  3. 两者之间不存在 Happens-Before 关系

注意这里最后一点可以推导出两个操作一定属于不同的 threads,因为同一个 thread 的 events 之间存在 po 关系

若 execution graph 是 racy 的,那么其 outcome 可以为任意值,即未定义行为

C11 consistency

7af70e8d9cf94039bc4650a8b95dd340.png

最后需要考虑在 C11 内存模型中定义 SC 的语义

56ef229a50a1452ea183502566b60f20.png

由于对一个 atomic 变量的访问存在多种 modes (except na),目前并没有一个完善的定义

对一个普通变量的访问就是 na

3c204d7bc8c04adb8c0935cdd53fcabc.png

上述内容摘自 Repairing sequential consistency in C/C++11

Exercise

为每一次内存访问添加 access modes

ceb61a9cf39b4c008e8c1c88af7b5d2e.png

这个例子在之前的 Atomic Weapons 中出现过,主要关注 a.count 这个变量

4b83e4522cf54b28b34324f9307eca68.png

TODO

Weak Memory Concurrency in C/C++11 and LLVM

9cb1ed3fcc0149408e09a840c93e7ccc.png

这一节主要批判一下 C11 内存模型,然后引入 promising semantics

Overview

31303a5ae65f45d2bf916463c8eaaf77.png

上面是两个常见的编译器优化,其中 hoisting 是利用了 cmov 条件传送指令,以减少分支预测错误的处罚

b0d3d3de4ab344268a985b557853cb7e.png

然而,如果结合上述两种优化,就可能让一个没有 race 的程序产生 race

例如若 c 为 false,那么对 x 的访问就会出现 data race

49505b4811054672a468fcb5cf8cd7f6.png

8473423c13264a37a5b2f40b47954083.png

后面三点在 Memory Models 一节的 Design Criteria 中提过,第一点单调性 (monotone) 在本节 slide #22 会给出解释

dade6a8eafc44c84ab20fc0e7601f79a.png

2b1e4d97c241401799906facc873d1c7.png

之前讲过了 Operational Semantics 和 Declarative Semantics,后者便是 Axiomatic 的方法,这里略过 Transformational 的方法

e2e44dd6700d48618208b8ffc344f45a.png

0c65a3ee7a3c4ee78b625ce4ae7561fd.png

076994bf8035452aa11ba9b15a464cf2.png

The C11 memory model

38e0b88ddd864dd0a7b758614cc7dcd8.png

d639f8afd099463cbc336edd22198860.png

再次强调,对于 non-atomic 变量,data race 会产生未定义行为,而对于 atomic,就需要考虑可能的 data race 及其合理的行为

5413a9d5cddf4002ab46fc549ec638d2.png

dc6040f5426945dc932df8871400fafb.png

注意对变量赋初值是 na access mode,此处 rel 和 acq 形成 sw

f3925d2e2fec47409206d67cb4693fcd.png

对于 rlx 版本的 store buffering,C11 内存模型是允许的

46f164f7447e4f02a5c1a284fb2e0710.png

对同一个原子变量的 rlx 读写保证不会乱序

Unfortunate flaws in C11

下面是一个例子,说明了 C11 并不保证单调性

a78eab990e6b40aca199a9fe11b6456b.png

在 rlx 读写下,上述构成的 execution graph 无环,所以是被允许的

c70a652654fc492398adeb8ff821c53b.png

然而,在 na 读写下,由于 non-atomic read axiom,若发生上述行为,要求 W x 1R x 1 之间要存在 hb 边,然而这是两个不同的 threads,并不存在 hb 边,所以是不可能的

对于 non-atomic read axiom 的说明

<a, b> ∈ rf
mode(b) = na
根据 race 的定义,若不产生 race,必定有 <a, b> ∈ hb 或 <b, a> ∈ hb
若为后者,则 hb ∪ rf 会产生环
所以只可能是 <a, b> ∈ hb

647234a4384e45d29c368f6f0f3a9e35.png

总之 C11 内存模型存在一堆问题

下面还有两个例子

80ddcdd816684ca29ceafffa02321abe.png

这个例子说明了对于同一个变量不同 access modes 的访问也存在问题

所以这里可能读取的值并不合理

930d3e14ddfe46c8a01063d72082b98d.png

这个例子说明了 na read 会阻止一些可能发生的行为

类似上一个例子,若 R a 1,那么 W a 1R a 1 之间应该存在 hb 边,然而这是两个不同的 threads,并不存在 hb 边,所以分支总是为 false,也就破坏了这一节 slide #16 的结构

然而,若串行化前两个线程,由于存在 po 边,W a 1R a 1 之间就存在 hb 边了,程序的行为反而增加了

79e74e7500e44e9d8b3154b30cad9c11.png

这实际上就是单调性的丧失,也就是在同步之后程序的行为反而增加了

The OOTA problem

79d19814f62543528a9e2990cc09ce15.png

之前提过,在 HMM 内存模型下会出现 OOTA 问题,在 C11 内存模型中同样会出现该问题

db6e571d3ad44985bc391a949e9ad2c5.png

C11 内存模型允许 load buffering 行为

1a24bcf3299f427da143cce1b90b41d5.png

只需略加修改 load buffering,便可以产生 OOTA 问题,此处 a = x 实际上可以读到任意值,然而 execution graph 中并未出现环

另一方面,对于下面的例子,两个线程之间并不存在 race,但却出现了未定义行为,也就违反了 DRF guarantee

3dabece6a8c7411d9588a3d2bc576962.png

在某种程度上,硬件可以解决部分的 OOTA 问题,例如记录数据的依赖关系,加入到 execution graph 中,判断是否存在环,然而会存在误判的情形

A promising solution to OOTA

5a3182f1551e415f871bd0af52158d3a.png

这里通过操作语义提出了一个新的弱内存模型,下面是两个例子

6393eb0a0a48447887738d8d8aeae60c.gif

内存现在不是一个 map,而是一些 message 的有序集,对于同一个内存地址而言

在 store buffering 的例子中,在 thread #1 的视角下,对 x 的写最新的 timestamp 为 1,而对 y 的写最新的 timestamp 为 0,所以可以读取到 y = 0,thread #2 与之对称

e8c4408b29d349e2953c2812a265bee5.gif

在另一个例子中,在 thread #1 的视角下,对 x 的写最新的 timestamp 为 2,实际上 thread #2 对 x 的写记录的 timestamp 可以在 (0, 1) 之间,此时在 thread #1 的视角下,对 x 的写最新的 timestamp 就变成 1 了

然而,在 thread #2 的视角下,若 thread #1 先写 (timestamp 更小),那么 thread #2 对 x 的写最新的 timestamp 总是自己记录的 timestamp,所以不可能读到 x 为 1

同理,若 thread #2 先写,那么 thread #1 也不可能读到 x 为 2,所以上述情形不可能发生

下面引入 promise 的概念,一个线程可以将未来的一个写操作添加到内存中,这不会改变该线程记录的最大的 timestamp,但要求在未来的某一时刻,该线程需要完成该写操作,即改变该线程记录的最大的 timestamp

1add00fd98cb4a78a6c03a2a07ecdb6e.gif

在 load buffering 的例子中,thread #1 在执行前 promise 了 y = 1,添加了 y: 1@1 这个 message,从而 thread #2 能够读取 y 的值,最后,thread #1 更新记录 y 的最大的 timestamp 为 1,所以上述情形是被允许的

e7cfaf46fab94d199afe18a277ae321a.png

这里 promise 能够解决 OOTA 问题关键在于,promise 所承诺的写操作,在线程本地的上下文中,是可以实现的

f37604c9758f44719115446833f38de3.png

例如对于最下面的例子,在 thread #1 开始执行前,x 的值为 0,是无法承诺 y = 1

下面几个可以加深对 promise 的理解

9e5040b610d2446ab25da3144aa72631.png

这里若 promise x = 1,假设 timestamp 为 1,那么在 a = x 读取 x 的值时,就会更新记录 x 的最大的 timestamp 为 1,此时需要 fulfill 之前的 promise,然而之前定义写操作要求该 timestamp 要大于该线程记录的最大的 timestamp,所以这里无法 fulfill,上述行为不被允许

baad368407ac4e08adf5c03542cc9fb3.png

对于上面的例子,若添加两个线程,行为就被允许了,其核心在于在 a = x 读取 x 的值时,所更新的 timestamp 不是之前 promise 的 timestamp,而是 thread #3 进行写操作时设置的 timestamp,若该 timestamp 小于 promise 的 timestamp,x = 1 的 promise 就能成功 fulfill 了

考虑串行化前两个线程,实际上需要 promise x = 1y = 1,由于不会在 thread #1 中读取 y 的值,所以不会影响 y = 1 这个 promise 的 fulfill

a2340592b9994403b5c21e71cb6f2cb2.png

然而,这种内存模型下并不是单调的,略加修改上面的例子

先考虑右边,需要 promise y = 1,注意这里 x 的初值为 0,所以分支为 true,promise 在本地上下文中是实现的,在实际的 fulfill 过程中,y 所读取的 x 为 1,实际上发生在 a = x 中,而非分支中

再考虑左边,由于不在一个线程中,无法 promise y = 1,即使 thread #0 promise x = 1,由于 a 最终的值为 1,所以分支为 false,无法执行到写操作对应的指令

e7698ad9c3e1493faacdf100b5fcd1c2.png

目前只考虑了 na access,完整的内存模型还需要考虑很多东西

7621bb50d53f4acc884d200436c1d34c.gif

以 rel 和 acq access modes 为例,当一个写操作为 rel 时,需要在 message 中添加当前线程的 view,这样另一个线程 acq 对应的写操作时,可以应用其余变量的 timestamp,进而实现同步

3f45bb9f3bfc43d7a1ab77dbd72e7b1f.png

394fb64a684c44d3be932694aab1f27a.png

Concurrent Objects

Safety and Liveness

Safety (Correctness)

those properties whose violation always has a finite witness

if something bad happens on an infinite run, then it happens already on some finite prefix

Liveness (Progress)

those properties whose violation never has a finite witness

no matter what happens along a finite run, something good could still happen later

Correctness

Sequential Specifications

Example: Pre and PostConditions for Dequeue

  • Precondition:
    • Queue is non-empty
  • Postcondition:
    • Returns first item in queue
  • Postcondition:
    • Removes first item in queue

  • Precondition:

    • Queue is empty
  • Postcondition:

    • Throws Empty exception
  • Postcondition:

    • Queue state unchanged

Sequential vs. Concurrent



Linearizability intuition

Formal Model of Executions

Invocation Notation

A q.enq(x)

Response Notation

A q:void

History

Describing an Execution

H

A q.enq(3)
A q:void
A q.enq(5)
B p.enq(4)
B p:void
B q.deq()
B q:3

Match

Invocation & response match if

Projections

H|q

A q.enq(3)
A q:void
A q.enq(5)
B q.deq()
B q:3

H|A

A q.enq(3)
A q:void
A q.enq(5)

Complete Subhistory

An invocation is pending if it has no matching respnse

no unmatched Invocation & response

Complete(H)

A q.enq(3)
A q:void
B p.enq(4)
B p:void
B q.deq()
B q:3

Sequential Histories

Method calls do not interleave

Well-Formed Histories

Per-thread projections sequential, avoid

A q.enq(3)
A q.enq(4)

Equivalent Histories

Threads see the same thing in both, ∀A. H|A = G|A

Sequential Specifications

Precedence

A method call precedes another if response event precedes invocation event

Given history H, method executions m0 and m1 in H, We say m0 ->H m1, if m0 precedes m1

Relation ->H is a partial order, total order if H is sequential

Linearizability

History H is linearizable if it can be extended to G by

So that G is equivalent to legal sequential history S, where ->G ⊆ ->S

legal defined by sequential specification

1874bb81b44044d8a0fcf9e15d8d3140.png

S respects “real-time order” of G

Composability Theorem

History H is linearizable if and only if, for every object x, H|x is linearizable

Linearization point

Identify one atomic step where method happens

Might need to define several different steps for a given method

Example:

public T deq() throws EmptyException {
lock.lock();
try {
if (tail == head)
throw new EmptyException();
T x = items[head % items.length];
head++;
return x;
} finally {
lock.unlock();
}
}

the moment of unlock

Sequential Consistency

History H is sequential consistency if it can be extended to G by

So that G is equivalent to legal sequential history S

No ->G ⊆ ->S

No need to preserve real-time order

Often used to describe multiprocessor memory architectures

Sequential Consistency does not have composability

视整个 memory 为一个 object,若该 object 满足 sequential consistency,则该 memory model 是 SC 的

Example:

16d6616ec9ac480d852aebbbda874613.png

实际上是 store buffering

Initially: x = y = 0
// thread 1
x = 1;
r1 = y; // 0
// thread 2
y = 1;
r2 = x; // 0

H|xH|y 都是 SC 的,但是 H 是 TSO,是弱于 SC 的

Progress

Progress Conditions

Non-BlockingBlocking
Everyone makes progressWait-freeStarvation-free
Someone makes progressLock-freeDeadlock-free

blocking progress conditions depend on fair scheduler

Summary

We will look at linearizable blocking and non-blocking implementations of objects.

Linked lists

Target

Four Patterns

Set Interface

public interface Set<T> {
public boolean add(T x);
public boolean remove(T x);
public boolean contains(T x);
}

List Node

public class Node {
public T item;
public int key;
public Node next;
}

Representation Invariant

Abstraction Map

Given a list satisfying rep invariant, S(head) = { x | there exists a such that a reachable from head and a.item = x}

Fine-Grained Synchronization

由于 Coarse-Grained Locking 的策略,即一把大锁保平安,lock 的 contention 过于严重

考虑将 lock 拆分,即 Fine-Grained Synchronization

基本思想是持有两把 locks,通过交替 lock 和 unlock,在 list 中搜索然后完成相应的操作

下面是一个并发 remove 的例子

d04c1506570e494ebd7dd159f2b13fbb.gif

Linearization point 只需选取持有对应的 locks 之后实际对 list 进行操作的任意时刻即可

Progress 的性质取决于 head 节点上 lock 的性质

Optimistic Synchronization

基本思想

validate 需要考虑下面两点

  1. 给定的节点是否仍然可达
  2. prev 是否依然指向 cur

Lazy Synchronization

Like optimistic, except

主要思想是为每一个节点添加一个 field,用于表示该节点是否被 marked delete,在做实际的 delete 操作前,设置该 field,在 SC 内存模型下,其余线程就可以得知自己 scan 到的节点是否仍然存在,从而避免了 rescan

2fb25c710ca146bcb7d869d07cf59d8b.gif

validate 需要考虑下面两点

  1. prev 和 cur 是否被 marked delete
  2. prev 是否依然指向 cur

还需要更新 Abstraction Map 的定义

Given a list satisfying rep invariant, S(head) = { x | there exists a such that a reachable from head, a.item = x and a is unmarked}

注意到此时的 contains 操作实际上是 wait-free

Lock-Free Synchronization

避免使用 lock,考虑使用 CAS 操作

例如删除 cur 所在的节点,只需如下操作

prev.next.CAS(cur, succ);

然而存在一种情形,即 prev 已经被 marked delete,此处上述操作就会出现问题

于是在进行 CAS 操作的时候需要同时考虑 mark 字段

JAVA 提供了 AtomicMarkableReference

这样,在删除 cur 所在的节点时,需要如下操作

if (!prev.next.attemptMark(cur, true)) {
continue;
}
Node<T> succ = cur.next.getReference();
prev.next.compareAndSet(cur, succ, true, false);
return true;

这里的想法是,每个线程在 scan 的过程会 active 的 physical remove 已经被 logical remove 的节点,避免因为一个线程因为某些原因始终无法 physical remove 对应的节点,进而形成类似 lock 的效应

对于 add 方法,当 physical add 操作失败,代表 list 的结构已发生变化,需要重新 scan

于是,在 contains 操作是 wait-free 的基础上,add 和 remove 操作变成了 lock-free

在性能方面,Lock-Free Synchronization 相较于 Lazy Synchronization 提升并不明显,因为算法更加复杂了

Queues

Bounded, Blocking, Lock-based Queue

这里 blocking 的含义是

很显然,线程之间消息的传递需要使用条件变量

由于 enq 操作和 deq 操作分别在 queue 的两端进行,所以需要两把 locks每把 lock 对应一个条件变量

以 enq 操作为例,deq 操作与其完全对称

简单起见,这里 queue 的大小使用一个原子变量记录,自增和自减该原子变量会在 enq 线程和 deq 线程之间形成 contention

一种解决方案是,enq 操作和 deq 操作分别维护一个变量 queue 的大小,当变量 overflow 或 underflow 时,同时获取 enq lock 和 deq lock,以此同步 queue 的真实大小

Unbounded, Non-Blocking, Lock-free Queue

这里 non-blocking 在 unbounded 语境下的含义是

考虑使用 CAS 操作修改 queue,以 enq 为例,需要修改两处地方

显然一次 CAS 操作只能修改一个地方,若两次 CAS 操作之间被其他线程打断,queue 就会陷入异常的状态

这里的想法和 Lock-Free 类似,即

enq 操作的核心代码可以参考

while (true) {
Node<T> last = tail.get();
Node<T> next = last.next.get();
// optimize
if (last != tail.get()) {
continue;
}
if (next == null) {
if (last.next.compareAndSet(null, node)) {
tail.compareAndSet(last, node);
return;
}
} else {
// help
tail.compareAndSet(last, next);
}
}

当 logical enq 成功之后,即第 11 行的分支为 true,此后到 return 之间的任意时刻均可作为 linearization point

deq 操作的核心代码可以参考

while (true) {
Node<T> first = head.get(), last = tail.get();
Node<T> next = first.next.get(); // <--
// optimize
if (first != head.get()) {
continue;
}
if (first == last) {
if (next == null) {
// empty
throw new EmptyException();
}
// help
tail.compareAndSet(last, next);
} else if (head.compareAndSet(first, next)) {
return next.item;
}
}

这里比较 tricky 的地方在于,对于 throw exception 情形下 linearization point 的选择

若选择第 11 行的分支为 true 之后的某一时刻作为 linearization point,由于可能在该分支之后其余线程执行了 enq 操作,所以并不合理

实际上应当选取第 3 行原子的读取到 next 为 null 的时刻作为 linearization point,同时保证在这一轮能够执行到 throw exception 那一行,即该 linearization point 是 future-dependent

The Dreaded ABA Problem

在上述 Lock-free Queue 的流程中考虑手动管理内存

一个简单的想法是,在全局维护一个 free list,其中存放了 unused queue nodes

由于 CAS 操作只会比较 node 的地址,而不会比较该地址中存放的内容,所以可能出现 Dreaded ABA Problem

26fc1883c7b144d8a7acc74a3f4dd20f.gif

这里的流程是,一个线程想要进行 deq 操作,然后该线程睡眠,期间有另一个线程进行了一些 deq 和 enq 操作,导致原来 head 指针指向的 node 被回收,然后又被 reuse 成为 head 指针指向的 node,此时该 node 其中的内容已经发生了改变,但是由于该 node 的地址没有发生变化,所以刚开始的线程进行的 CAS 操作仍然会成功,从而使 queue 陷入异常的状态

出现 Dreaded ABA Problem 的根本原因是 CAS 指令的语义,如果使用 Load-link/store-conditional 指令,在 store 的时候会检查从 load 到 store 这段时间内,该地址存放的内容是否发生了变化,使用 LL/SC 代替 CAS 实现上述 Lock-free Queue,就不会出现 Dreaded ABA Problem

如果只能使用 CAS 操作,类似 Lock-Free List 中的想法,在进行 CAS 操作的时候考虑对应的 stamp 字段,该字段记录了该地址存放的内容被修改的时间

JAVA 提供了 AtomicStampedReference

Stacks

Lock-free Stack

类似 Lock-free Queue,不过由于 push 和 pop 仅需一次 CAS 操作,所以实现会简单很多

对比 enq 和 push,enq 需要设置 tail.nexttail,而 push 仅需要设置 top,对于 node.next = top 的设置并不会修改 stack 本身

若 push 或 pop 失败,该线程会随机的 sleep 一段时间 (backoff),然后重试该请求

Lock-free Stack 不是 wait-free 的,可能存在某个调度序列,导致某个请求始终无法返回

Linearization point 只需选取对 stack 实际进行修改的时刻即可

Lock-free Stack 一些缺陷

Elimination-backoff Stack

考虑上述 Lock-free Stack 的第二点缺陷,当 CAS 操作失败时,线程会随机的 sleep 一段时间,然后重试该请求,为了避免 contention,随着重试次数增加,sleep 的时间也可以相应增加

观察到一种现象,即存在两个线程配对的 push 和 pop,由于 CAS 操作失败,两个线程都会 sleep

实际上可以考虑让 push 的线程将 value 存放在一个 array 随机的一个位置,然后等待 pop 的线程用 null value 与该 value 交换,从而实现 stack 的语义

对称的,pop 的线程将 null value 存放在一个 array 随机的一个位置,然后等待 push 的线程用 value 与该 null value 交换,同样可以实现 stack 的语义

更具体的

这样可以优化 backoff 的纯 sleep 带来的并发度的下降

对于具体的交换操作,有一些参数可以设置

上述算法的核心在于 elimination array 中一个 slot 的实现,考虑使用无锁的方式实现,称之为 Lock Free Exchanger

每个 slot 存在着如下状态

由于状态数超过了 2,具体实现可以考虑使用 AtomicStampedReference

状态间的转移如下,细节详见代码注释

T item = slot.get(stamp); // 首先读取状态
switch (stamp[0]) {
case EMPTY: {
if (!slot.compareAndSet(item, myItem, EMPTY, WAITING)) { // 若 CAS 操作失败,代表该 slot 已经被其余线程修改,重试
break;
}
while (System.currentTimeMillis() < timeBound) { // 在时限内不断检测是否有另一个线程交换了 value
item = slot.get(stamp);
if (stamp[0] == BUSY) { // 若有,则重置状态,并返回交换的 value
slot.set(null, EMPTY);
return item;
}
}
if (slot.compareAndSet(item, null, WAITING, EMPTY)) { // 超时,试图重置状态
throw new TimeoutException();
}
item = slot.get(stamp); // 重置失败,代表在超时后有另一个线程交换了 value,于是重置状态,并返回交换的 value
slot.set(null, EMPTY);
return item;
}
case WAITING: {
if (slot.compareAndSet(item, myItem, WAITING, BUSY)) { // 该 slot 中存在 value 正在等待被交换,尝试进行交换操作
return item;
}
break;
}
case BUSY: { // 有两个线程正在使用该 slot 进行交换,直接返回
break;
}
default:
assert false;
}

对于 Safety 和 Liveness 的分析如下

The Relative Power of Synchronization Operations

The Art of Multiprocessor Programming Chapter 5

wait free 的基础上,考虑各种 current objects 能够实现几个线程之间的 consensus

简单起见,这里的 consensus 所 decide 的 value 定义为第一个 propose 的线程所提供的 value

注解

  1. Lock-Free vs. Wait-free

Any wait-free implementation is lock-free.

Lock-free is the same as wait-free if the execution is finite.

下述的一系列结论在 lock free 的基础上也成立

  1. Consensus is universal

See Chapter 6

Consensus Numbers

一个 current object 对应的 consensus number n 定义如下

可以证明 atomic read-write registers 的 consensus number 为 1

FIFO Queue

我们想要了解能否只使用 atomic read-write registers 实现一个 FIFO Queue (multiple dequeuers or multiple enqueuers)

single-enqueuer and single-dequeuer 的 Queue 可以通过 atomic read-write registers 实现

public class WaitFreeQueue {
int head = 0, tail = 0;
Item[size] items;
public void enq(Item x) {
while (tail - head == QSIZE) {}
items[tail % QSIZE] = x;
tail++;
}
public Item deq() {
while (tail - head == 0) {}
Item item = items[head % QSIZE];
head++;
return item;
}
}

考虑使用反证法,如果能够通过 FIFO Queue 实现 2-thread consensus,那么 atomic read-write registers 的 consensus number 就为 2,矛盾

所以需要考虑如何通过 FIFO Queue 实现 2-thread consensus

其构造并不困难,两个线程并发的对 FIFO Queue 执行 deq 操作,所 decide 的 value 定义为取到 FIFO Queue 的第一个元素的线程所提供的 value 即可

这也代表 FIFO Queue 的 consensus number 至少为 2

Multiple Assignment Theorem

Atomic registers cannot implement multiple assignment

其证明思路也是反证法,take 3-element array

可以证明 double assignment 的 consensus number 至少为 2

6fde77bfcefe411bb90652b192ad2854.gif

Read-Modify-Write

考虑在硬件层面提供一种特殊的寄存器,该寄存器支持 RMW 操作,可以抽象为

public abstract class RMWRegister {
private int value;
synchronized public int getAndMumble() {
int prior = this.value;
this.value = mumble(this.value);
return prior;
}
}

其中的 mumble 是一个 function

下面给出 non-trivial 的定义

A RMW method with function mumble(x) is non-trivial if there exists a value v such that v ≠ mumble(v)

显然对于普通的 read,其对应的 RMW object 是 trivial 的

可以证明 non-trivial RMW object 的 consensus number 至少为 2

public class RMWConsensus
extends ConsensusProtocol {
private RMWRegister r = v;
public Object decide(object value) {
propose(value);
if (r.getAndMumble() == v)
return proposed[i];
else
return proposed[j];
}
}

其中的初值 v 要求 v ≠ mumble(v)

有一类特殊的 RMW objects,其 mumble functions 属于 F\mathcal{F}

Let F\mathcal{F} be a set of functions such that for all fif_i and fjf_j, either

  • Commute: v.fi(fj(v))=fj(fi(v))\forall v.f_i(f_j(v))=f_j(f_i(v))

  • Overwrite: v.fi(fj(v))=fi(v)\forall v.f_i(f_j(v))=f_i(v)

可以证明它们的 consensus number 为 2

对于 FAA,其 mumble function 满足 Commute 性质,所以其对应的 RMW object 的 consensus number 为 2

对于 CAS,可以证明其对应的 RMW object 的 consensus number 为 \infty

public class RMWConsensus
extends ConsensusProtocol {
private AtomicInteger r =
new AtomicInteger(-1);
public Object decide(object value) {
propose(value);
r.compareAndSet(-1, i);
return proposed[r.get()];
}
}

这里 i 代表线程编号,所以选取 -1 为初值