考虑以下示例:

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() 的获取“过时”。

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

    最佳答案

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

    最简单的方法是使用 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 修饰符自动执行的操作。

    10-05 21:19