易失性字段:如何实际获取字段的最新写入值?

时间:2014-03-14 11:46:26

标签: c# .net multithreading .net-4.5 volatile

考虑以下示例:

private int sharedState = 0;

private void FirstThread() {
    Volatile.Write(ref sharedState, 1);
}

private void SecondThread() {
    int sharedStateSnapshot = Volatile.Read(ref sharedState);
    Console.WriteLine(sharedStateSnapshot);
}

直到最近,我的印象是,只要FirstThread()确实在SecondThread()之前执行,该程序就无法输出任何内容,只有 1

但是,我现在的理解是:

  • Volatile.Write()发出一个发布栏。这意味着在 1 分配给sharedState后,可能不会发生前面的加载或存储(按程序顺序)。
  • Volatile.Read()发出获取围栏。这意味着在将sharedState复制到sharedStateSnapshot之前,不会发生后续加载或存储(按程序顺序)。

或者,换句话说:

  • sharedState实际发布到所有处理器核心时,该写入之前的所有内容也将被释放,并且,
  • 获取地址sharedStateSnapshot中的值时;必须已经获得sharedState

如果我的理解是正确的,那么如果sharedState中的写入尚未被释放,则无法阻止获取FirstThread()过时' / p>

如果这是真的,我们如何才能确保(假设最弱的处理器内存模型,如ARM或Alpha),程序将始终打印 1 ? (或者我在某个地方的心理模型中犯了错误?)

3 个答案:

答案 0 :(得分:6)

您的理解是正确的,确实无法确保程序始终使用这些技术打印1。为了确保程序打印1,假设线程2在第一个线程之后运行,则每个线程需要两个栅栏。

实现这一目标的最简单方法是使用lock关键字:

private int sharedState = 0;
private readonly object locker = new object();

private void FirstThread() 
{
    lock (locker)
    {
        sharedState = 1;
    }
}

private void SecondThread() 
{
    int sharedStateSnapshot;
    lock (locker)
    {
        sharedStateSnapshot = sharedState;
    }
    Console.WriteLine(sharedStateSnapshot);
}

我想引用Eric Lippert

  坦率地说,我不鼓励你做一个不稳定的领域。易失性字段表明你正在做一些非常疯狂的事情:你试图在两个不同的线程上读取和写入相同的值,而不需要锁定。

这同样适用于调用Volatile.ReadVolatile.Write。实际上,它们甚至比volatile字段更糟糕,因为它们要求您手动执行volatile修饰符自动执行的操作。

答案 1 :(得分:4)

您是对的,并不能保证所有处理器都能立即看到发布商店。 Volatile.ReadVolatile.Write为您提供获取/释放语义,但没有即时保证。

volatile修饰符似乎可以做到这一点。编译器将发出OpCodes.Volatile IL指令,抖动将告诉处理器不要将变量存储在任何寄存器上(参见Hans Passant's answer)。

但是为什么你还需要它立即?如果您的SecondThread恰好在实际写入值之前的几毫秒内运行了怎么办?由于时间安排是非确定性的,因此您的计划的正确性不应该取决于这个"即时性"反正。

答案 2 :(得分:2)

  

直到最近,我的印象是,只要   FirstThread()确实在SecondThread()之前执行了这个程序   不能输出任何东西,只有1。

当你继续解释自己时,这种印象是错误的。 Volatile.Read只是对其目标发出读取操作,然后是内存屏障;内存屏障阻止了执行当前线程的处理器上的操作重新排序,但这在这里没有用,因为

  1. 没有重新排序的操作(只是每个线程中的单个读取或写入)。
  2. 线程中的竞争条件意味着即使在处理器之间应用了无重新排序保证,也只是意味着无法预测的操作顺序将被保留。
  3.   

    如果我的理解是正确的,那么没有什么可以做的   如果写入,则阻止获取sharedState为“陈旧”   FirstThread()尚未发布。

    这是正确的。从本质上讲,您使用的工具旨在帮助弱内存模型解决由竞争条件引起的可能问题。该工具无法帮助您,因为它不是它的功能。

      

    如果这是真的,我们怎样才能真正确保(假设最弱)   程序将使用的处理器内存模型,如ARM或Alpha)   总是打印1? (或者我在心理模型中犯了错误   某处?)

    再次强调:内存模型不是问题所在。为确保您的程序始终打印1,您需要做两件事:

    1. 提供显式线程同步,保证在读取之前进行写入(在最简单的情况下,SecondThread可以对FirstThread用来表示已完成的标志使用自旋锁。
    2. 确保SecondThread不会读取陈旧值。您可以通过将sharedState标记为volatile来轻松完成此操作 - 虽然此关键字当之无愧,但它是专门为此类用例设计的。
    3. 所以在最简单的情况下,你可以举例:

      private volatile int sharedState = 0;
      private volatile bool spinLock = false;
      
      private void FirstThread()
      {
          sharedState = 1;
          // ensure lock is released after the shared state write!
          Volatile.Write(ref spinLock, true); 
      }
      
      private void SecondThread()
      {
          SpinWait.SpinUntil(() => spinLock);
          Console.WriteLine(sharedState);
      }
      

      假设没有其他两个字段的写入,则保证此程序只输出1。