2009-01-15 47 views
1

以下构造是线程安全的,假设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; 
    } 
} 

这显然乍看上去不安全,所以我还是要强调,为什么我认为这可能是安全的:

  1. 只有两个选项是FOO的元素将保持不变或将设置为MAGIC_VAL。
  2. 如果线程2在更新中看到foo [i]处于中间状态,则只会发生两件事:中间状态为< SOME_OTHER_NUMBER或不是。如果它是< SOME_OTHER_NUMBER,则线程2也会尝试将其设置为MAGIC_VAL。如果不是,线程2将不会执行任何操作。

编辑:另外,如果foo是一个长或双或什么,以便更新它不能做原子?你可能仍然认为对齐等是更新foo的一个元素不会影响任何其他元素的。此外,在这种情况下,多线程的全部重点都是性能,所以任何类型的锁定都会打败它。

回答

0

如果读取和写入每个数组元素都是原子的(即它们正确对齐,并且没有像前面提到的那样撕裂文字),那么在这段代码中应该没有任何问题。如果foo[i]小于SOME_NUMBERSOME_OTHER_NUMBER中的任一个,则至少一个线程(可能两者)将在某个点将其设置为MAGIC_VAL;否则,它将不受影响。随着原子读写,没有其他的可能性。

但是,由于您的情况更加复杂,请非常小心 - 确保foo[i]真正只在每个循环中读取一次并存储在局部变量中。如果您在同一次迭代中多次阅读它,则可能会得到不一致的结果。即使您对代码进行的一点点修改都可能会立即使其对于竞争条件不安全,因此大量评论带有大红色警告标志的代码。

0

这是不好的做法,你不应该处于两个线程同时访问同一个变量的状态,而不管后果如何。你给的例子是过于简化,任何大多数复杂的样本几乎总会有与之相关的问题.. ...

记住:信号量是你的朋友!

0

该特定示例是线程安全的。

这里没有真正涉及的中间状态。 该特定的程序不会感到困惑。尽管如此,我建议在阵列上使用互斥体

1

您可以通过比较和交换操作安全且无锁地执行此操作。你有什么看起来线程安全,但编译器可能会在某些情况下创建一个未更改的值的写回,这将导致一个线程在另一个线程上。

此外,你可能没有获得尽可能多的性能,因为这样做会导致CPU缓存内部的MESI transitions风暴,每个线程都是相当的慢。有关多线程内存一致性的更多详细信息,请参阅Ulrich Drepper的“​​”的第3.3.4节。

+0

是的,对于这样一个简单的for循环来说,在两个线程中执行它可能比仅仅执行一个循环慢,因为它们共享数据并且(尤其是不好的)写入它。负面可扩展性的万岁! – 2009-01-15 22:38:13

4

在现代多核处理器上,您的代码不是线程安全的(至少在大多数语言中)没有内存障碍。简而言之,没有明确的障碍,每个线程都可以从缓存中看到foo的完全不同的副本。

假设你的两个线程在某个时间点运行,那么在稍后的某个时间点,第三个线程会读取foo,它可能会看到一个完全未初始化的foo或其他两个线程中的任何一个的foo,或者两者兼有,这取决于CPU内存缓存发生了什么。

我的建议 - 不要试图对并发性“聪明”,总是尽量保持“安全”。聪明会每一次咬你。 broken double-checked locking文章有一些令人大开眼界的见解,说明在没有内存障碍的情况下(尽管专门针对Java和它(改变)的内存模型,它对任何语言都很有见地),内存访问和指令重新排序会发生什么。

你必须真正在你的语言的指定内存模型之上进行快捷屏障。例如,Java允许变量被标记为volatile,与一个被记录为具有原子分配的类型结合,可以允许非同步分配并通过将其强制转移到主存储器来获取(所以线程没有观察/更新缓存副本) 。

相关问题