下面的构造是否是线程安全的,假设foo的元素是正确对齐和大小的,以便没有单词撕裂?如果没有,为什么不呢?
注意:下面的代码是我想要做的玩具示例,而不是我真实的现实场景。显然,在我的例子中有更好的方法来编码可观察行为。
uint[] foo;
// Fill foo with data.
// In thread one:
for(uint i = 0; i < foo.length; i++) {
if(foo[i] < SOME_NUMBER) {
foo[i] = MAGIC_VAL;
}
}
// In thread two:
for(uint i = 0; i < foo.length; i++) {
if(foo[i] < SOME_OTHER_NUMBER) {
foo[i] = MAGIC_VAL;
}
}
乍一看这显然看起来不安全,所以我要强调为什么我认为它可以是安全的:
SOME_OTHER_NUMBER
或者不是。如果是&lt; SOME_OTHER_NUMBER
,第二个线程也会尝试将其设置为MAGIC_VAL。如果没有,第二个线程将不执行任何操作。编辑:另外,如果foo是long或double或者什么,那么更新它不能以原子方式完成?你可能仍然认为对齐等是这样的,更新foo的一个元素不会影响任何其他元素。此外,在这种情况下,多线程的全部意义在于性能,因此任何类型的锁定都会使其失败。
答案 0 :(得分:4)
在现代多核处理器上,您的代码不是线程安全的(至少在大多数语言中)没有内存屏障。简单地说,没有明确的障碍,每个线程都可以从缓存中看到foo的不同完整副本。
假设您的两个线程在某个时间点运行,然后在稍后的某个时间点,第三个线程读取foo,它可能会看到一个完全未初始化的foo,或者其他两个线程中的任何一个的foo,或者两者的混合,取决于CPU内存缓存发生的情况。
我的建议 - 不要试图对并发性“聪明”,总是尽量保持“安全”。聪明每次都会咬你。 broken double-checked locking文章对于在没有内存障碍的情况下内存访问和指令重新排序会发生什么会有一些令人大开眼界的见解(尽管特别是关于Java及其(改变)内存模型,它对任何语言都很有洞察力。)< / p>
您必须真正掌握语言指定的内存模型以快捷方式。例如,Java允许变量被标记为volatile,它与记录为具有原子赋值的类型相结合,可以通过强制它们进入主内存来允许不同步的赋值和获取(因此线程不会观察/更新缓存的副本)
答案 1 :(得分:1)
您可以使用比较和交换操作安全无锁地执行此操作。您所拥有的看起来线程安全,但编译器可能会在某些情况下创建未更改值的写回,这将导致一个线程踩到另一个线程。
此外,你可能没有像你想象的那样获得那么多的性能,因为让这两个线程写入相同的连续内存会导致CPU缓存中的MESI transitions风暴,每个这很慢。有关多线程内存一致性的更多详细信息,请参阅Ulrich Drepper的“What Every Programmer Should Know About Memory”部分3.3.4。
答案 2 :(得分:0)
如果对每个数组元素的读写都是原子的(即它们正确对齐,没有像你提到的那样撕裂),那么这段代码应该没有任何问题。如果foo[i]
小于SOME_NUMBER
或SOME_OTHER_NUMBER
,则至少有一个帖子(可能是两个)会在某个时刻将其设置为MAGIC_VAL
;否则,它将不受影响。使用原子读取和写入,没有其他可能性。
但是,由于您的情况更复杂,所以要非常小心 - 确保foo[i]
每个循环只读取一次并存储在局部变量中。如果您在同一次迭代中多次读取它,则可能会得到不一致的结果。即使您对代码做出的最轻微改动也可能立即使其在竞争条件下变得不安全,因此对具有大红色警告标志的代码进行大量评论。
答案 3 :(得分:0)
这是不好的做法,你不应该处于两个线程同时访问同一个变量的状态,而不管后果如何。您给出的示例过于简化,任何大多数复杂样本几乎总会出现与之相关的问题...... ...
请记住:信号量是您的朋友!
答案 4 :(得分:0)
该特定示例是线程安全的。
这里没有真正涉及的中间国家。 那个特别的节目不会混淆。
我建议在数组上使用 Mutex 。