以下是Leslie Lamport撰写的文章Teaching Concurrency中的简单并发程序。
考虑从0到$ N-1 $编号的$ N $进程,其中每个进程$ i $执行
x[i] := 1
y[i] := x[(i - 1) % N]
并停止,其中每个x[i]
最初等于0.(假设每个x[i]
的读写都是原子的。)
此算法满足以下属性:每个进程停止后,y[i]
至少等于1
一个过程$ i $。它很容易验证:写y[i]
的最后一个$ i $进程必须
将其设为1.
然后,Lamport评论说
但是这个过程没有将
y[i]
设置为1,因为它是写$ y $的最后一个过程。算法满足此属性,因为它保持归纳不变。你知道那个不变量是什么吗?如果没有,那么你不完全理解为什么算法满足这个属性。
因此,
并发程序的归纳不变量是什么?
答案 0 :(得分:4)
x
s模型具有以下行为:x[i]
当前1
为i
当且仅当流程x
已经运行时。当然,在所有流程完成后,所有1
都设置为y
。
y[i]
有点棘手:x[i-1]
设置为y[i]
,即1
为i
当且仅当<当i
正在写y
时,y[i]
的前任已经运行。
程序不变量为:如果某个流程已设置x[i]
,则必须已将1
设置为y[i]
。无论0
是设置为1
还是y
,都是如此。
证明这个不变量非常简单:一开始,y[i]
s都没有设置,所以它很简单。在程序执行期间,每次写入x[i]
都sequenced after写入y[i]
。因此,不变量也适用于该程序的每一步。
进一步的推理是这样的:完成的最后一个过程将1
设置为y
,因为根据最后一个过程的定义,它的前任必须已经在那时完成执行(即。其x
值已设置。这意味着,由于不变量,其y
值(确定最后一个流程&#39; 1
值)必须为y
。
查看它的另一种方法:找不到所有0
设置为y
的执行顺序。这样做将要求所有进程在其前任之前执行。但是,由于我们的进程被排列成一个环(也就是说,如果我遵循前一个链,我最终会再次出现在我的起点),这会导致至少一个进程在写入之前必须完成执行的矛盾。 {{1}}。
答案 1 :(得分:4)
该程序的归纳不变量,用TLA+语法表示:
/\ \A i \in 0..N-1 : (pc[i] \in {"s2", "Done"} => x[i] = 1)
/\ (\A i \in 0..N-1 : pc[i] = "Done") => \E i \in 0..N-1 : y[i] = 1
归纳不变量是满足以下两个的不变量 条件:
Init => Inv
Inv /\ Next => Inv'
其中:
Inv
是归纳不变的Init
是描述初始状态的谓词Next
是描述状态转换的谓词。请注意,归纳不变量仅与当前状态和下一状态有关。它 没有引用执行历史, 它不是关于系统过去的行为。
并发系统原理和规范的第7.2.1节 (通常称为The TLA+ Hyperbook), Lamport描述了为什么他更喜欢使用归纳不变量而不是行为 证明(即那些参考执行历史的证据)。
行为证明可以更正式,但我不知道任何实际的方法 使它们完全正式 - 也就是说,写出可执行的描述 真实算法和形式行为证明它们满足正确性 属性。这是超过35年写作的原因之一 并发算法,我发现行为推理是不可靠的 更复杂的算法。我相信行为的另一个原因 证明本质上比基于状态的证据更加复杂 复杂的算法。这导致人们写出不那么严格的行为 这些算法的证明 - 特别是没有完全正式的证明 作为指南。
为了避免错误,我们必须从国家的角度思考, 不是在执行方面......仍然,行为推理提供了一个 不同的思考算法的方式,思考总是有帮助的。 只有在使用行为推理而不是基于状态的情况下,行为推理才是错误的 推理而不是除此之外。
我们感兴趣的属性是(在TLA +语法中):
(\A i \in 0..N-1 : pc[i] = "Done") => \E i \in 0..N-1 : y[i] = 1
我在这里使用描述每个控件状态的PlusCal约定 使用名为&#34; pc&#34;的变量的过程(我认为它是&#34;程序计数器&#34;)。
此属性是不变的,但它不是归纳不变量,因为它 不满足上述条件。
您可以使用归纳不变量通过编写如下所示的证据来证明财产:
1. Init => Inv
2. Inv /\ Next => Inv'
3. Inv => DesiredProperty
要想出一个归纳不变量,我们需要为每一步给出标签 算法,让他们称呼他们&#34; s1&#34;,&#34; s2&#34;和&#34;完成&#34;,其中&#34;完成&#34;是个 每个过程的终端状态。
s1: x[self] := 1;
s2: y[self] := x[(self-1) % N]
当程序处于倒数第二个(倒数第二个)状态时,请考虑该程序的状态 执行。
在上一个执行状态中,pc[i]="Done"
表示i的所有值。在里面
倒数第二个状态,pc[i]="Done"
表示i的所有值,除了一个,让我们调用它
j,其中pc[j]="s2"
。
如果进程i在&#34;完成&#34;状态,那么x[i]=1
必须是真的,因为该过程必须执行语句&#34; s1&#34;。
类似地,进程j处于状态&#34; s2&#34;也必须执行
声明&#34; s1&#34;,所以x[j]=1
必须是真的。
我们可以将其表达为一种不变量,这恰好是一种归纳法 不变的。
\A i \in 0..N-1 : (pc[i] \in {"s2", "Done"} => x[i] = 1)
为了证明我们的不变量是一个归纳不变量,我们需要一个合适的模型
Init
状态谓词和Next
状态谓词。
我们可以从PlusCal中描述算法开始。这很简单 算法,所以我称之为&#34;简单&#34;。
--algorithm Simple
variables
x = [i \in 0..N-1 |->0];
y = [i \in 0..N-1 |->0];
process Proc \in 0..N-1
begin
s1: x[self] := 1;
s2: y[self] := x[(self-1) % N]
end process
end algorithm
我们可以将PlusCal模型转换为TLA +。这就是我们的样子 将PlusCal翻译成TLA +(我已经省略了终止条件,因为 我们在这里不需要它。)
------------------------------- MODULE Simple -------------------------------
EXTENDS Naturals
CONSTANTS N
VARIABLES x, y, pc
vars == << x, y, pc >>
ProcSet == (0..N-1)
Init == (* Global variables *)
/\ x = [i \in 0..N-1 |->0]
/\ y = [i \in 0..N-1 |->0]
/\ pc = [self \in ProcSet |-> "s1"]
s1(self) == /\ pc[self] = "s1"
/\ x' = [x EXCEPT ![self] = 1]
/\ pc' = [pc EXCEPT ![self] = "s2"]
/\ y' = y
s2(self) == /\ pc[self] = "s2"
/\ y' = [y EXCEPT ![self] = x[(self-1) % N]]
/\ pc' = [pc EXCEPT ![self] = "Done"]
/\ x' = x
Proc(self) == s1(self) \/ s2(self)
Next == (\E self \in 0..N-1: Proc(self))
\/ (* Disjunct to prevent deadlock on termination *)
((\A self \in ProcSet: pc[self] = "Done") /\ UNCHANGED vars)
Spec == Init /\ [][Next]_vars
=============================================================================
请注意它如何定义Init
和Next
状态谓词。
我们现在可以指定我们想要证明的归纳不变量。我们也想要我们的 归纳不变意味着我们有兴趣证明的财产,所以我们 将它添加为连接。
Inv == /\ \A i \in 0..N-1 : (pc[i] \in {"s2", "Done"} => x[i] = 1)
/\ (\A i \in 0..N-1 : pc[i] = "Done") => \E i \in 0..N-1 : y[i] = 1
Init => Inv
显然为什么会这样,因为如果Inv
为真,Init
中的前提都是假的。
Inv /\ Next => Inv'
(\A i \in 0..N-1 : (pc[i] \in {"s2", "Done"} => x[i] = 1))'
有趣的案例是某些i pc[i]="s1"
和pc'[i]="s2"
。通过
s1
的定义,应该清楚为什么这是真的。
((\A i \in 0..N-1 : pc[i] = "Done") => \E i \in 0..N-1 : y[i] = 1)'
有趣的案例是我们之前讨论的案例,其中pc[i]="Done"
为。{1}}
i的所有值,除了一个j,其中pc[j]="s2"
。
通过Inv的第一个结合,我们知道x[i]=1
代表i的所有值。
按s2
,y'[j]=1
。
Inv => DesiredProperty
在这里,我们想要的属性是
(\A i \in 0..N-1 : pc[i] = "Done") => \E i \in 0..N-1 : y[i] = 1
请注意,我们只是将我们感兴趣的财产归还给了 不变,所以这是微不足道的证明。
您可以使用TLA+ Proof System(TLAPS)进行编写 一个正式的证据,可以机械检查,以确定它是否正确。
这是我使用TLAPS编写和验证的证明,该TLAPS使用归纳不变量 证明所需的财产。 (注意:这是我写过的第一个证明 使用TLAPS,所以请记住这是由新手写的。)
AtLeastOneYWhenDone == (\A i \in 0..N-1 : pc[i] = "Done") => \E i \in 0..N-1 : y[i] = 1
TypeOK == /\ x \in [0..N-1 -> {0,1}]
/\ y \in [0..N-1 -> {0,1}]
/\ pc \in [ProcSet -> {"s1", "s2", "Done"}]
Inv == /\ TypeOK
/\ \A i \in 0..N-1 : (pc[i] \in {"s2", "Done"} => x[i] = 1)
/\ AtLeastOneYWhenDone
ASSUME NIsInNat == N \in Nat \ {0}
\* TLAPS doesn't know this property of modulus operator
AXIOM ModInRange == \A i \in 0..N-1: (i-1) % N \in 0..N-1
THEOREM Spec=>[]AtLeastOneYWhenDone
<1> USE DEF ProcSet, Inv
<1>1. Init => Inv
BY NIsInNat DEF Init, Inv, TypeOK, AtLeastOneYWhenDone
<1>2. Inv /\ [Next]_vars => Inv'
<2> SUFFICES ASSUME Inv,
[Next]_vars
PROVE Inv'
OBVIOUS
<2>1. CASE Next
<3>1. CASE \E self \in 0..N-1: Proc(self)
<4> SUFFICES ASSUME NEW self \in 0..N-1,
Proc(self)
PROVE Inv'
BY <3>1
<4>1. CASE s1(self)
BY <4>1, NIsInNat DEF s1, TypeOK, AtLeastOneYWhenDone
<4>2. CASE s2(self)
BY <4>2, NIsInNat, ModInRange DEF s2, TypeOK, AtLeastOneYWhenDone
<4>3. QED
BY <3>1, <4>1, <4>2 DEF Proc
<3>2. CASE (\A self \in ProcSet: pc[self] = "Done") /\ UNCHANGED vars
BY <3>2 DEF TypeOK, vars, AtLeastOneYWhenDone
<3>3. QED
BY <2>1, <3>1, <3>2 DEF Next
<2>2. CASE UNCHANGED vars
BY <2>2 DEF TypeOK, vars, AtLeastOneYWhenDone
<2>3. QED
BY <2>1, <2>2
<1>3. Inv => AtLeastOneYWhenDone
OBVIOUS
<1>4. QED
BY <1>1, <1>2, <1>3, PTL DEF Spec
请注意,在使用TLAPS的证明中,您需要有一个类型检查不变量(它上面称为TypeOK
),您还需要处理&#34;口吃状态&#34;其中没有变量发生变化,这就是为什么我们使用[Next]_vars
代替Next
。
Here's a gist完整的模型和证明。