假设我们有以下代码:
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字段...
答案 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 由任何其他线程按执行顺序执行的写入。但符合要求的实施是 不需要提供易失写入的单个总排序 从所有执行线程中可以看出。
让我们看看它的样子:
因此,我们有两个半栅栏:一个用于易失性写入,一个用于易失性读取。他们并没有保护我们不要重新安排他们之间的指示。
而且,即使在像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;
}
这将通过添加全围栏来保护我们免于重新排序这些说明。