我试图提出一种互斥算法,该算法仅基于原子读取和共享存储器的原子写入(即没有比较和交换或类似).
除了相互排斥和僵局自由之外,还需要满足以下特性:
>面对任何时刻死亡的过程,它都需要强大
>它需要处理竞争锁定的事前未知数量的进程
>每隔几秒我就需要一个进程来进入关键部分(我不关心哪个)
>它应尽可能保持内存效率
>我的所有进程共享一个合理同步的时钟源(亚秒精度)
我想出了一种方式,让我觉得它可以工作,但我想让你们运行它,看看我是否在某个地方犯了错误,或者是否有更优雅的解决方案.
我们的想法是将Szymanski’s Three-Bit Linear Wait Algorithm (Figure 1 on page 4 of “Mutual Exclusion Revisited”)的修改版本与基于Moir and Anderson’s “Wait-Free Algorithms for Fast, Long-Lived Renaming” (M&A)的松散程序命名方案相结合.
作为第一步,我通过为它们分配像M& A这样的ID来定义传入过程的“顺序”:
M& A使用以下互斥原语(参见第5页的图2):
在进入块的n个进程中,最多1个将停在那里,最多n – 1将向右移动,并且最多n – 1将向下移动.
这些原语可以链接在一起形成一个网格,我决定用不同于M& A的方式对方框进行编号,以便我可以在不知道方框总数的情况下计算方框编号:
可以使用box =(m *(m-1))/ 2 r计算箱号,其中m是总移动次数(包括移动到左上方框中,即m≥1),r是向右移动的次数(r≥0).
最初将为任何新进程分配一个大的,全局唯一的ID(例如,时间戳巨大的随机数),它将在上面的互斥原语算法中用作p的值.然后它将按照基元的规则开始遍历网格,从左上角开始,直到它停在某个框中.它停止的框的编号现在将是其新进程ID.
为了保持进程ID的数量更小(进程可能会消失;而且:如果所有进入的进程向右或向下移动并且没有停止,则可以永久锁定框而不使用),我将替换上面原语中的布尔变量Y带有时间戳.
然后将Y:= true行替换为Y:= now,其中now为当前时间.类似地,如果Y然后…的条件将被替换为if(现在 – Y< timeout)然后…,其中timeout是一个框将被返回到可用池的时间. 当进程仍在运行时,它需要将其框的Y值设置为现在永远不允许其ID到期.虽然较小的超时值会导致较小的ID和较少的内存使用,但理论上可以选择任意大,因此应始终可以找到超时值,以确保进程永远不会丢失其ID,除非它们实际退出或死了.分钟,小时甚至天的值应该可以解决问题. 现在,我们可以使用此流程顺序来实现Szymanski的算法:
communication variables:: a, w, s: boolean = false
private variables:: j: 0..n
p1 ai=true;
p2 for(j=0;j<n;j++)
while(sj);
p3 wi=true;
ai=false;
p4 while(!si) {
p5 for (j=0;j<n & !aj;j++);
p6 if (j==n) {
si=true;
p6.1 for (j=0;j<n & !aj;j++);
p6.2 if (j<n)
si=false;
p6.3 else {
wi=false;
p6.4 for(j=0;j<n;j++)
while(wj);
}
}
p7 if (j<n)
for (j=0;j<n & (wj | !sj);j++);
p8 if (j!=i & j<n) {
p8.1 si=true;
wi=false;
}
}
p9 for(j=0;j<i;j++)
while(wj | sj);
Critical Section
e1 si=false;
这个算法背后的想法有点牵扯和简洁(如果这还不是一个失败的原因),我不会试图在这里再次解释它(Wikipedia also discusses a variant of this algorithm).
为了使该算法根据需要工作(即面对进程中止和未知的进程总数),可以进行以下更改:
就像上面的Y一样,布尔ai,wi和si将被时间戳替换.除此之外,这次超时需要足够短,以便仍然可以根据需要经常输入关键部分,在我的情况下每隔几秒,因此5秒的超时可能会起作用.同时,超时需要足够长,以便除非进程终止,否则它永远不会过期,即它需要比进程持续更新需要保持设置的标志的时间戳所花费的最坏情况时间更长.
进程上的循环(例如,对于(j = 0; j >这看起来是否可靠?我从未做过像这样的框架的正式证明.我的直觉(和强烈的思考)告诉我它应该是坚实的,但我不介意比那更严格的东西.我最担心的是在Szymanski算法的p3和e1之间死亡的过程的影响以及随机过期的标志,而其他过程都在代码的那一部分.
>有没有更好的解决方案(更简单/更少的资源使用)?优化的一个攻击点可能是我不需要所有进程最终进入关键部分,我只需要一个(我不喜欢)不关心哪一个).
对不起这篇文章的篇幅,谢谢你的耐力阅读吧!建议缩短欢迎… 🙂
PS:我也在CS stack exchange上发布了这个问题.
最佳答案 由于我并不真正关心公平或饥饿(哪个进程进入临界区并不重要),我真的不需要使用像Szymanski那样复杂的算法.
我发现了一个非常漂亮的选择:Burns和Lynch的算法:
program Process_i;
type flag = (down, up);
shared var F : array [1..N] of flag;
var j : 1..N;
begin
while true do begin
1: F[i] := down;
2: remainder; (* remainder region *)
3: F[i] := down;
4: for j := 1 to i-1 do
if F[j] = up then goto 3;
5: F[i] := up;
6: for j := 1 to i-1 do
if F[j] = up then goto 3;
7: for j := i+1 to N do
if F[j] = up then goto 7;
8: critical; (* critical region *)
end
end.
您可以在他们的论文“Mutual Exclusion Using Indivisible Reads and Writes”的第4页(836)找到它.
它有几个主要优点:
>这简单得多
>它使用更少的内存(事实上,他们声称他们的算法在这方面是最佳的)
>所有共享内存只有一个编写器(当然是多个读者)
>很容易将忙碌等待转为延迟重试(见here)
旁白:这个算法的想法可以看作如下:
我是一个进程,我和其他人(所有其他进程)连续站在某个地方.当我想进入关键部分时,我会执行以下操作:
> 3/4:只要我看到有人伸出手,我就向左看并保持手.
> 5:如果左边没有人举手,我就放了.
> 6:我再次检查左边是否有人举手.如果是这样,我把我放回去重新开始.否则,我举起手来.
> 7:我右边的每个人都先行,所以我向右看,等到我看不到任何举手.
> 8:一旦我的右手都向下,我可以进入关键部分.
> 1:当我完成后,我把手放回去.
我用这个算法实现了上面的整个想法(包括M& A),我目前正在测试它的存在,到目前为止看起来非常稳定.
实施非常直接.实际上只有两个额外的警告我必须考虑(除非,当然,我错过了一些东西(指针欢迎)):
>如果一个进程在算法中进入第7行,它可能最终命中goto,在这种情况下,在我的实现中(见here),临界区的条目被拒绝,进程稍后再次尝试.当重试发生时(可能有很大的延迟),我需要刷新进程的标志,使得它甚至没有标志过期的最微小的时刻,或者,如果它已经过了,我需要检测然后跳回到算法中的第3行,而不是继续第7行.
>我添加了一个检查是否需要输入关键部分(即限制输入关键部分的速率的声明).在进程甚至尝试进入临界区之前执行此检查是最有效的.但是,为了100%确定没有线程可以超过速率限制,当进程成功进入临界区时,需要进行第二次检查.
由于我的代码使用的共享数据结构,它目前还没有真正处于使其在这里发布有意义的状态,但是通过一些工作,我可以组合一个自包含的版本.如果有人有兴趣,请发表评论,我会这样做.