当工作线程非竞争地写入本地或类变量时,是否需要锁定或易失性?

cro*_*sek 4 c# variables locking volatile task

对于下面的情况,当工作线程之间没有写入竞争时,仍然需要锁或易失性?如果"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)?
}
Run Code Online (Sandbox Code Playgroud)

Eri*_*ert 7

安全的做法是首先不要这样做.不要在一个线程上写一个值,并在第一个位置读取另一个线程上的值.创建一个Task<object>和一个Task<int>将值返回给需要它们的线程,而不是创建跨线程修改变量的任务.

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

正如Brian Gideon在他的回答中指出的那样,你得到了一个记忆障碍WaitAll,但我不记得,如果这是一个记录保证或只是一个实现细节.

正如我所说,我不会首先这样做.如果我被迫,我至少会把我写的变量标记为volatile.


Ser*_*rvy 5

写入引用类型(即Object)和字大小的值类型(即int在32位系统中)是原子的.这意味着当你查看值(位置6)时,你可以确定你得到的是旧值或新值,而不是其他东西(如果你有一个类型,如大型结构,它可以被拼接,并且你可以在写完一半时阅读这个值.你并不需要一个lockvolatile,只要你愿意接受的阅读过时的值的潜在风险.

请注意,因为此时没有引入内存屏障(lock或者volatile两者都添加一个),所以变量可能已在另一个线程中更新,但当前线程没有观察到该更改; 它可以在(可能)在另一个线程中被更改之后的一段时间内读取"陈旧"值.使用volatile将确保当前线程可以更快地观察变量的变化.

WaitAll即使没有lock或,您也可以确保在通话后获得适当的价值volatile.

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

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