Solution of a Problem in Concurrent Programming Control
原文URL:http://web.cs.wpi.edu/~cs502/cisco11/Papers/Dijkstra_ConcurrentProgrammingControl.pdf
译文URL:http://duanple.com/?p=1022
导读
这篇论文是 Dijkstra 写的一篇解决并发编程中存在的互斥问题:在多个独立程序需要共享某个资源的时候,如何确保任意时刻只有一个程序能进行临界区访问。如果你对并发编程有一些了解的话,看到这个问题你第一反应应该是——锁。锁是什么,在现实角度来看,是一个防止外人接触内部资源的东西,加了锁会使我的资源变安全;从计算机的角度而言,锁是防止一个程序访问共享资源时,出现另一个程序同时访问造成共享资源数据混乱。 在读这篇文章前,我强烈建议你们都去看一遍原文+译文,去领略一下前人的思想,再回来看这篇文章,或许你会有所收获。
过程
第一遍读的是英文版的,读完全文后我的脑子只有一片浆糊,因为完全看不懂,不仅仅是英文,还有他想表达的意思。
第二遍读的是中文版,我还是没看懂,尽管是中文,但给我的理解是,他是在解决一个问题,这个问题是什么...我不理解,因为我没读懂题目是什么意思。
第三遍我使用的 GPT 进行解读,整体看了一遍,大致了解了一下,给我的感觉是锁的前身。
全文梳理解读
在这我斗胆将自己对这篇论文的见解进行分享
一、问题模型
论文首先描述了一个抽象的场景,我们需要将 computer 替换成进程进行理解
原文:
To begin, consider N computers, each engaged in a process which, for our aims, can be regarded as cyclic. In each of the cycles a so-cMled "critical section" occurs and the computers have to be programmed in such a way that at any moment only one of these N cyclic processes is in its critical section. In order to effectuate this mutual exclusion of critical-section execution the computers can communicate with each other via a common store. Writing a word into or nondestructively reading a word from this store are undividable operations; i.e., when two or more computers try to communicate (either for reading or for writing) simultaneously with the same common location, these communications will take place one after the other, but in an unknown order.
译文:
假设有N个计算机,每个计算机内部都有一个进程,方便起见,可以认为这些进程都是在不断地循环执行一段逻辑。在每个执行周期内都有一个“critical-section”,需要以某种方式对这些计算机进行编程,以确保任意时刻这个N个进程中只有一个会处于“critical-section”。为了实现临界区的互斥执行,这些计算机相互之间可以通过一个共享存储模块进行通信。针对该存储模块的一个以word为单位的读写操作可以认为是原子的,比如当有两个或更多的计算机试图同时读写同一个存储单元时,这些操作会以未知顺序依次完成。
总结来看,他提出的问题是:系统中有 N 个循环执行的并发进程,每个进程都会周期性进入临界区。目标是保证任意时刻最多只有一个进程进入临界区。进程之间通过共享内存通信,并且对单个共享变量的读写是原子的,但多个并发操作的实际执行顺序不确定。
二、模型约束
然后就是进行约束,在我看来进行约束的目的是给 互斥 这个问题提供一个正确、通用、可靠的解决方案。 依旧来看原文译文
原文:
(a) The solution must be symmetrical between the N computers; as a result we are not allowed to introduce a static priority.
(b) Nothing may be assumed about the relative speeds of the N computers; we may not even assume their speeds to be constant in time.
(c) If any of the computers is stopped well outside its critical section, this is not allowed to lead to potential blocking of the others.
(d) If more than one computer is about to enter its critical section, it must be impossible to devise for them such finite speeds, that the decision to determine which one of them will enter its critical section first is postponed until eternity. In other words, constructions in which "After you"-"After you"-blocking is still possible, although improbable, are not to be regarded as valid solutions.
译文:
(a) 解决方案必须在 N 台计算机之间对称;因此,我们不允许引入静态优先级。
(b) 不得对 N 台计算机的相对速度做任何假设;我们甚至不能假设它们的速度随时间保持不变。
(c) 如果任何一台计算机在远超出其临界区的位置停止运行,则不允许导致其他计算机发生潜在的阻塞。
(d) 如果多台计算机即将进入其临界区,则必须不可能为它们设计出如此有限的速度,以至于决定哪台计算机首先进入临界区的决定被无限期地推迟。换句话说,即使“你之后”-“你之后”阻塞的可能性很小,但这种构造仍然不能被视为有效的解决方案。
意思就是:不能为某个进程设置固定的静态优先级;不能依赖进程之间的相对执行速度;进程在非临界区停止时不能阻塞其他进程;多个进程竞争临界区时必须最终产生结果,不能陷入无限互相礼让的活锁状态。
三、解决方案
原文:

// 所有进程共享的内存
var b [N + 1]bool
var c [N + 1]bool
var k int
func init() {
// 所有布尔值初始为 true
for i := 1; i <= N; i++ {
b[i] = true
c[i] = true
}
// k 的初始值只要在 1~N 范围内即可
k = 1
}
// 第 i 个进程执行的程序
func process(i int) {
var j int
L0:
// 我准备竞争临界区
b[i] = false
L1:
// 当前候选者不是自己
if k != i {
// 暂时退出最终竞争状态
c[i] = true
// 如果当前候选者 k 没有参与竞争,
// 则把自己设置为新的候选者
if b[k] == true {
k = i
}
// 重新检查候选者
goto L1
}
// 当前候选者已经是自己
c[i] = false
// 检查是否还有其他进程处于最终竞争状态
for j = 1; j <= N; j++ {
if j != i && c[j] == false {
goto L1
}
}
// 只有通过上面的检查,才能进入临界区
criticalSection(i)
// 离开临界区,恢复初始状态
c[i] = true
b[i] = true
// 非临界区。
// 进程允许在这里暂停或停止,不会阻塞其他计算机。
remainderSection(i)
goto L0
}
四、证明
原文:
We start by observing that the solution is safe in the sense that no two computers can be in their critical sectionsimultaneously. For the only way to enter its critical section is the performance of the compound statement L14 without jumping back to L11, i.e., finding all other c's true after having set its own c to false.
The second part of the proof must show that no infinite "After you" - "After you" - blocking can occur; i.e., when none of the computers is in its critical section, of the computers looping (i.e., jumping back to L11) at least one - and therefore exactly one - will be allowed to enter its critical section in due time.
If the kth computer is not among the looping ones, b[k] will be true and the looping ones will all find k ≠ i. As a result one or more of them will find in L23 the Boolean b[k] true and therefore one or more will decide to assign "k := i". After the first assignment "k := i", b[k] becomes false and no new computers can decide again to assign a new value to k. When all decided assignments to k have been performed, k will point to one of the looping computers and will not change its value for the time being, i.e., until b[k] becomes true, viz., until the kth computer has completed its critical section. As soon as the value of k does not change any more, the kth computer will wait (via the compound statement L14) until all other c's are true, but this situation will certainly arise, if not already present, because all other looping ones are forced to set their c true, as they will find k ≠ i. And this, the author believes, completes the proof.
译文:
首先,我们观察到该解决方案是安全的,因为没有两台计算机能够同时处于临界区。进入临界区的唯一方法是执行复合语句 L14,而不跳回 L11,也就是说,在将自身的 c 设置为 false 之后,找到所有其他 c 都为 true。
证明的第二部分必须表明不会出现无限的“你之后”-“你之后”阻塞;也就是说,当没有计算机处于临界区时,在循环的计算机(即跳回 L11)中,至少有一台(因此恰好有一台)会在适当的时候进入临界区。
如果第 k 台计算机不在循环的计算机之列,则 b[k] 为 true,并且所有循环的计算机都会发现 k ≠ i。因此,其中一台或多台计算机会在 L23 中发现布尔值 b[k] 为 true,因此其中一台或多台计算机会决定赋值“k := i”。在第一次赋值“k := i”之后,b[k]变为假,此时没有新的计算机可以再次决定为k赋值。当所有已决定的k赋值都执行完毕后,k将指向某个循环计算机,并且暂时不会改变其值,直到b[k]变为真,也就是直到第k台计算机完成其临界区。一旦k的值不再改变,第k台计算机将等待(通过复合语句L14),直到所有其他c都为真。这种情况必然会出现(如果尚未出现),因为所有其他循环计算机都被迫将其c设置为真,因为它们会发现k ≠ i。作者认为,这便完成了证明。
如果你看了第一遍你没有看懂,是正常的
解读
先来讲解一下变量,b, c 两个布尔类型的数组,k整型变量 ,变量的语义采用的反直觉语义,也就是当 b[i] = false 时进程 i 申请进入临界区,非常的反直觉,所以不那么直观。
b[]:谁还在参与竞争
k :当前优先让谁尝试进入
c[]:谁已经进入最终检查阶段
整个流程可以梳理为
所有想进入临界区的进程
|
| b[i] = false
v
第一阶段:争夺候选资格 k
|
| k == i
v
第二阶段:设置 c[i] = false,检查其他 c[j]
|
| 所有其他 c[j] 都为 true
v
进入临界区
第一阶段的目标不是立刻决定谁已经获得锁,而是先稳定出一个候选者。进程 i 进入竞争后,先执行 b[i] = false。如果它发现 k != i,说明当前候选者不是自己,就将 c[i] 设置为 true,然后退出竞争位置,然后检查 b[k]。如果 b[k] == true,说明当前候选者已经不再参与竞争了,那么进程 i 就可以执行 k = i,将自己变成新的候选者。如果 b[k] == false,说明当前候选者还在竞争,其他进程不能直接取代它,只能继续等待。
这里最关键是:多个进程可能同时看到 b[k] == true,然后都决定执行 k = i。比如有进程 1、2、3、4,它们都已经进入竞争状态,并且都读取到 b[k] == true,因此都决定修改 k。假设这些写操作按照 2、3、1、4 的顺序执行,当进程 2 率先完成 k = 2 后,新的 k 已经指向一个处于竞争状态的进程,此时 b[k] == b[2] == false。从这一刻开始,没通过判断的新进程不会再决定修改 k。但此前已经读取到旧值还是 b[k] == true、并且已经决定执行赋值操作的进程,仍然会继续完成写操作,例如进程 3、1、4 仍然会依次执行 k = 3、k = 1、k = 4。因此,最终的 k 不一定指向最先完成的进程 2,而是指向最后一个进程 4。当所有决定执行写操作的进程全部完成后,k 最终会稳定地指向某个仍处于竞争状态的进程。
当 k 稳定下来后,第二阶段开始发挥作用。假设最终 k = r,那么进程 r 是当前候选者。其他所有进程 i 都会发现 k != i,因此不断执行 c[i] = true,退出最终检查阶段。与此同时,因为 b[k] == b[r] == false,其他进程无法再次修改 k。候选进程 r 则执行 c[r] = false,然后检查所有其他进程的 c[j]。当它确认所有其他 c[j] 都为 true 时,才会进入临界区。
GPT总结:
这套设计承担两个责任。第一部分是互斥性,也就是 Safety:任意时刻不能有两个进程同时进入临界区。一个进程只有在先将自己的 c[i] 设置为 false,再确认所有其他 c[j] 都为 true 之后,才能进入临界区。并且它在离开临界区之前会一直保持 c[i] == false。假设进程 i 已经进入临界区,那么另一个进程 j 在进入之前扫描 c[i] 时,必然会发现 c[i] == false,因此只能跳回第一阶段,不能继续进入。即使两个进程几乎同时尝试进入第二阶段,也不可能同时成功:每个进程都必须先写入自己的 c,再读取对方的 c;在任意可能的原子操作顺序中,至少有一个进程会读到对方已经写入的 false,从而退出竞争。
第二部分是进展性,也就是 Progress:当临界区空闲且存在竞争者时,不能所有进程都永久循环,却始终没有人进入临界区。如果 k 指向一个不再竞争的进程,那么 b[k] == true,其他竞争者会尝试接管 k。经过有限次残留赋值后,k 最终稳定地指向某个活跃竞争者。此后,其他进程因为发现 k != i,会将自己的 c[i] 恢复为 true;候选进程最终能够观察到其他所有 c[j] 都为 true,于是进入临界区。原文中“至少一个,因此恰好一个”指的就是:进展性证明至少有一个进程最终可以进入,而互斥性证明同时进入的进程最多只有一个,两者合并后得到恰好一个。
Safety:不能有两个进程同时进入临界区
Progress:不能所有进程都一直竞争,但始终没有人进入临界区
总结
到这里,解读部分就完成了,其实我对于理论证明部分并不是特别敏感,我只能理解代码过程,以及这么做能带来的结果。像比如 GPT 总结的部分,我是不明白的,不管我怎么去想,我都想不出 Safety 和 Progress ,这两个是怎么总结出来的,也许这就是 AI 的魅力吧。