开始我们的Semaphore项目,我给了我的学生一个糟糕的p()方法版本:
proc p() {
while (this.tokens <= 0) {
sleep(1);
writeln("Tokens: ", this.tokens);
}
this.tokens -= 1;
}
我给他们一些额外的代码来测试它,它增加了另一个线程中的令牌数量(使用v()方法)。您可以看到令牌数量增加(大于0)但代码不会退出while循环。
一时兴起,我添加了一份作业声明:
proc p() {
while (this.tokens <= 0) {
this.tokens = this.tokens;
sleep(1);
writeln("Tokens: ", this.tokens);
}
this.tokens -= 1;
}
这解决了测试运行中的问题(尽管它的线程安全性更低)。为什么原始的while循环会卡住,为什么添加这个赋值会解决它?
答案 0 :(得分:4)
假设从不同的任务调用p()
和v()
,则无法保证在一个任务中对非原子this.tokens
的写入将被不同的任务看到。< / p>
一种解决方案是使tokens
原子化,并具有类似的东西:
proc p() {
while (this.tokens.read() <= 0) {
sleep(1);
writeln("Tokens: ", this.tokens.read());
}
this.tokens.sub(1);
}
当tokens
不是原子时,编译器可以自由地假设tokens
不会被其他任务修改,因此它可能会将您的代码转换为:
var tokenTemp = this.token;
while (tokenTemp <= 0)
...
并将写入插入token
可防止该提升。也就是说,即使写入token
,它仍然是无效/非法/未定义的代码,并且可能在将来被某些编译器/处理器重新排序轻易触发。
该代码是非法的,因为它违反了Chapel的内存一致性模型(MCM)。具体来说,这是一场数据竞赛,而Chapel只确保数据竞赛免费程序的顺序一致性。
内存一致性模型在语言规范中定义(https://chapel-lang.org/docs/1.16/_downloads/chapelLanguageSpec.pdf中的第30章)。它有一个很好的,非常容易获得的介绍段落。也就是说,由于它是一种语言规范,本章的其余部分非常干燥和技术性,所以它可能不是开发人员学习的最佳位置。
如需更短的概述,请查看https://chapel-lang.org/CHIUW/2015/hot-topics/01-Ferguson.pdf(特别是幻灯片10)。
除此之外,Chapel的内存模型基于C11 / C ++ 11,Java,UPC等。如果你寻找“C ++ 11内存模型”,“数据竞争免费程序”或“顺序一致性”,那里有很多很棒且易于访问的文章