4

对于下面的情况,当工作线程之间没有写竞争时,是否仍然需要锁或易失性?如果“G”不需要“Peek”访问,则答案有任何差异。

class A 
{
   Object _o; // need volatile (position A)?
   Int _i;    // need volatile (position B)?

   Method()
   {
      Object o;
      Int i;

      Task [] task = new Task[2]
      {
         Task.Factory.StartNew(() => { 
              _o = f1();   // use lock() (position C)?
              o  = f2();   // use lock() (position D)?
         } 
         Task.Factory.StartNew(() => { 
              _i = g1();   // use lock() (position E)?
              i  = g2();   // use lock() (position F)?
         }          
      }

      // "Peek" at _o, _i, o, i (position G)?

      Task.WaitAll(tasks);

      // Use _o, _i, o, i (position H)?
}
4

3 回答 3

7

安全的做法是首先不要这样做。不要在一个线程上写入一个值,然后首先在另一个线程上读取该值。制作 aTask<object>和 aTask<int>将值返回给需要它们的线程,而不是制作跨线程修改变量的任务。

如果您一心想跨线程写入变量,那么您需要保证两件事。首先,抖动不会选择会导致读写及时移动的优化,其次,引入了内存屏障。内存屏障限制处理器以某些方式及时移动读写。

正如 Brian Gideon 在他的回答中指出的那样,您会从 . 中获得内存屏障WaitAll,但我不记得这是一个书面保证还是只是一个实现细节。

正如我所说,我一开始不会这样做。如果我被迫这样做,我至少会将我正在写入的变量标记为易失性。

于 2013-07-22T19:14:47.177 回答
5

Object对引用类型(即)和字大小值类型(即int在 32 位系统中)的写入是原子的。这意味着,当您查看值(位置 6)时,您可以确定您获得的是旧值或新值,而不是其他值(如果您有诸如大型结构之类的类型,它可以被拼接,并且您可以在写入一半时读取该值)。您不需要lockor volatile,只要您愿意接受读取陈旧值的潜在风险即可 。

请注意,由于此时没有引入内存屏障(alockvolatile两者都使用 add one),因此变量可能已在另一个线程中更新,但当前线程没有观察到该更改;在另一个线程中更改它之后的相当长一段时间内,它可能会读取(可能)“陈旧”值。的使用volatile将确保当前线程可以更快地观察到变量的变化。

WaitAll即使没有lockor ,您也可以确保在调用 后您将获得适当的值volatile

另请注意,虽然您可以确定对引用类型的引用是以原子方式编写的,但您的程序不保证观察到的对引用所引用的实际对象的任何更改的顺序。即使从后台线程的角度来看,对象在分配给实例字段之前已被初始化,它也可能不会以该顺序发生。因此,另一个线程可以观察对对象的引用写入,然后跟随该引用并找到处于初始化或部分初始化状态的对象。引入内存屏障(即通过使用volatile变量可能允许您阻止运行时进行此类重新排序,从而确保不会发生这种情况。这就是为什么最好不要一开始就这样做并且只让这两个任务返回它们生成的结果,而不是操作一个封闭的变量。

WaitAll将引入内存屏障,除了确保两个任务实际完成之外,这意味着您知道变量是最新的并且不会具有旧的陈旧值。

于 2013-07-22T18:50:18.157 回答
1

在位置 G,您可以观察这些值_o,并_i可以分别保留它们的初始化值 null 和 0,或者它们可能包含任务写入的值。在这个位置上是不可预测的。

但是,在位置 H,您以两种不同的方式强制解决问题。首先,您已保证两个任务都已完成,因此写入已完成。其次,Task.WaitAll将生成一个内存屏障,以保证主线程将观察任务发布的新值。

因此,在这个特定示例中,显式锁或内存屏障生成器 ( volatile) 在技术上不是必需的。

于 2013-07-22T19:09:05.757 回答