【问题标题】:Why doesn't C# volatile protect write-read reordering?为什么 C# volatile 不保护读写重新排序?
【发布时间】:2012-07-31 18:56:00
【问题描述】:

根据this online book,C# 中的volatile 关键字不能防止重新排序写操作后跟读操作。它给出了这个例子,其中a 和b 最终都可以设置为0,尽管x 和y 是volatile:

class IfYouThinkYouUnderstandVolatile
{
  volatile int x, y;
 
  void Test1()        // Executed on one thread
  {
    x = 1;            // Volatile write (release-fence)
    int a = y;        // Volatile read (acquire-fence)
    ...
  }
 
  void Test2()        // Executed on another thread
  {
    y = 1;            // Volatile write (release-fence)
    int b = x;        // Volatile read (acquire-fence)
    ...
  }
}

这似乎符合 10.5.3 中的规范:

对易失性字段的读取称为易失性读取。易失性读取具有“获取语义”;也就是说,它保证在指令序列中对内存的任何引用之前发生。

对易失性字段的写入称为易失性写入。易失性写入具有“释放语义”;也就是说,它保证发生在指令序列中写指令之前的任何内存引用之后。

这是什么原因?是否存在我们不介意对写入-读取操作进行重新排序的用例?

【问题讨论】:

标签: c# .net multithreading volatile


【解决方案1】:

Volatile 不保证独立 volatile 变量的读取和写入不会重新排序,它只保证读取得到最新的值(非缓存)。 (保证对单个变量的读写保持顺序)

http://msdn.microsoft.com/en-us/library/x13ttww7%28v=vs.71%29.aspx

系统总是在请求时读取 volatile 对象的当前值,即使之前的指令要求来自同一对象的值也是如此。此外,对象的值在赋值时立即写入。

volatile修饰符通常用于多个线程访问的字段,而不使用lock语句来序列化访问。使用 volatile 修饰符可确保一个线程检索另一个线程写入的最新值。

当你有多个依赖操作时,你需要使用一些其他的同步机制。通常使用lock,这是最简单的,而且只会在被滥用或非常极端的情况下造成性能瓶颈。

【讨论】:

  • "Volatile 不保证读取和写入不会重新排序,它只保证读取得到最新的值(非缓存)。"我不认为这是对的。来自规范:“所有线程都将按照执行顺序观察任何其他线程执行的易失性写入”(取自stackoverflow.com/q/10589565)
  • @MaxBarraclough 也许我应该更清楚。是的,相同 volatile 变量的读/写保证按顺序排列。两个不相关变量的独立读/写(即表达式不相互依赖,如 OP 示例中的)没有相同的保证。
  • 这正是为什么当代码引用多个共享值时应该使用`lock`的原因(由于锁不需要是易失性的)。
【解决方案2】:

在 x86/x64 架构上使用 volatile read 防止 volatile write 重新排序将非常昂贵。这是因为名为@9​​87654323@ 的写入优化。 Java 采用了这种方式,Java 中的 volatile 写入实际上是 CPU 指令级别的完整内存屏障。

【讨论】:

    【解决方案3】:

    也许回答太晚了......但我环顾四周,看到了这个。 Volatile-read其实是调用了一个类似下面的方法:

    public static int VolatileRead(ref int address)
    {
        int num = address;
        Thread.MemoryBarrier();
        return num;
    }
    

    而Volatile-write是这样的:

    public static int VolatileWrite(ref int address, int value)
    {
        Thread.MemoryBarrier();
        adrdress = value;
    }
    

    指令MemoryBarrier(); 是防止重新排序的指令。MemoryBarrier(); 确保之前的指令在之后的指令之前执行。 当 VW 然后 VR,你将拥有:

    Thread.MemoryBarrier();
    adrdress = value; //this line may be reordered with the one bellow
    int num = address;//this line may be reordered with the one above
    Thread.MemoryBarrier();
    return num;
    

    【讨论】:

    • 正如您的回答所示,这个问题的答案实际上很简单:如果有人需要顺序一致性,可以随时在需要的地方插入额外的Thread.MemoryBarrier()。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-05
    • 1970-01-01
    • 2019-06-02
    • 1970-01-01
    • 2019-01-01
    • 1970-01-01
    相关资源
    最近更新 更多