.NET多线程,易失性和内存模型

时间:2010-04-15 11:51:21

标签: c# .net volatile memory-model

假设我们有以下代码:

class Program
 {
    static volatile bool flag1;
    static volatile bool flag2;
    static volatile int val;
    static void Main(string[] args)
    {
      for (int i = 0; i < 10000 * 10000; i++)
      {
        if (i % 500000 == 0)
        {
          Console.WriteLine("{0:#,0}",i);
        }

        flag1 = false;
        flag2 = false;
        val = 0;

        Parallel.Invoke(A1, A2);

        if (val == 0)
          throw new Exception(string.Format("{0:#,0}: {1}, {2}", i, flag1, flag2));
      }
    }

    static void A1()
    {
      flag2 = true;
      if (flag1)
        val = 1;
    }
    static void A2()
    {
      flag1 = true;
      if (flag2)
        val = 2;
    }
  }
}

这是错的!主要的问题是为什么......我认为CPU使用flag1 = true重新排序操作;和if(flag2)语句,但变量flag1和flag2标记为volatile字段...

2 个答案:

答案 0 :(得分:4)

在.NET内存模型中,运行时(CLI)将确保对volatile字段的更改不会缓存在寄存器中,因此可以在其他线程上立即看到对任何线程的更改( NB this在其他内存模型中并非如此,包括Java)。

但是,这并没有说明跨多个,不稳定或不可用的字段的操作的相对顺序。

要在多个字段之间提供一致的排序,您需要使用锁(或内存屏障,显式或隐式使用包含内存屏障的方法之一)。

有关详细信息,请参阅"Concurrent Programming on Windows", Joe Duffy, AW, 2008

答案 1 :(得分:0)

ECMA-335规范说:

  

易失性读取具有“获取语义”,这意味着读取是   保证在对发生的内存的任何引用之前发生   在CIL指令序列中的读指令之后。 :一种   volatile写有“释放语义”意味着写是   保证在写入之前的任何内存引用之后发生   CIL指令序列中的指令。一致的   CLI的实现应保证volatile的这种语义   操作。这可确保所有线程都会观察到volatile   由任何其他线程按执行顺序执行的写入。但符合要求的实施是   不需要提供易失写入的单个总排序   从所有执行线程中可以看出。

让我们看看它的样子:

enter image description here

因此,我们有两个半栅栏:一个用于易失性写入,一个用于易失性读取。他们并没有保护我们不要重新安排他们之间的指示。
而且,即使在像AMD64(x86-64)it is allowed stores to be reordered after loads这样的严格架构上也是如此。
对于硬件内存模型较弱的其他架构,您可以观察到更有趣的内容。在ARM上,如果以非易失方式分配参考,则可以观察到部分构造的对象 要修复您的示例,您应该在赋值和if子句之间放置Thread.MemoryBarrier()调用:

static void A1()
{
  flag2 = true;
  Thread.MemoryBarrier();
  if (flag1)
    val = 1;
}
static void A2()
{
  flag1 = true;
  Thread.MemoryBarrier();
  if (flag2)
    val = 2;
}

这将通过添加全围栏来保护我们免于重新排序这些说明。