理解CLR 2.0内存模型

14

Joe Duffy提出了描述CLR 2.0+内存模型的6条规则(实际实现,而非任何ECMA标准)。我正在尝试理解这些规则,主要是作为一种自我解释的方式,但如果我的逻辑有误,至少在这里有人能够在它引起麻烦之前抓住它。

  • 规则1:加载和存储之间的数据依赖关系永远不会被违反。
  • 规则2:所有存储都具有释放语义,即在其后不能移动任何加载或存储。
  • 规则3:所有易失性加载都是获取的,即在其前不能移动任何加载或存储。
  • 规则4:不得跨越完整屏障(例如Thread.MemoryBarrier、lock acquire、Interlocked.Exchange、Interlocked.CompareExchange等)进行加载和存储。
  • 规则5:堆上的加载和存储可能永远不会被引入。
  • 规则6:仅当从/到同一位置合并相邻的加载和存储时才可以删除加载和存储。

我正在尝试理解这些规则。

x = y
y = 0 // Cannot move before the previous line according to Rule 1.

x = y
z = 0
// equates to this sequence of loads and stores before possible re-ordering
load y
store x
load 0
store z

看起来,负载0可以移动到负载y之前,但是存储可能根本不会被重新排序。因此,如果一个线程看到z == 0,则它也会看到x == y。

如果y是易失性的,则负载0无法移动到负载y之前,否则它可能会。易失性存储似乎没有任何特殊属性,没有存储可以相互重新排序(这是非常强的保证!)

完整屏障就像是一条线,负载和存储不能越过。

不知道规则5是什么意思。

我猜规则6的意思是如果你这样做:

x = y
x = z

那么,CLR有可能删除对y的加载和对x的第一个存储。

x = y
z = y
// equates to this sequence of loads and stores before possible re-ordering
load y
store x
load y
store z
// could be re-ordered like this
load y
load y
store x
store z
// rule 6 applied means this is possible?
load y
store x // but don't pop y from stack (or first duplicate item on top of stack)
store z

如果y是易变的,会怎么样?我没有看到规则中禁止进行上述优化。这不违反双重检查锁定,因为在两个相同条件之间的lock()防止了负载被移动到相邻位置,并且根据规则6,这是它们可以被消除的唯一时间。
所以我认为我理解了除了规则5之外的所有内容。有人想启发我(或者纠正我或者添加任何关于上述内容的东西吗?)
1个回答

11

乔·达菲在Windows并发编程的第517-18页讨论了第5条规则:

作为引入负载的示例,请考虑以下代码:

MyObject mo = ...;
int f = mo.field;
if (f == 0)
{
    // do something
    Console.WriteLine(f);
}

如果从变量f中的mo.field的初始读取到后续在Console.WriteLine中使用f的时间间隔足够长,编译器可能会决定重新读取mo.field两次是更有效的。...如果mo是一个堆对象,并且线程正在同时写入mo.field,则这样做将是一个问题。if块可能包含假定读入f的值仍为0的代码,而读取操作可能会破坏这个假设。除了禁止volatile变量之外,.NET内存模型也禁止针对引用GC堆内存的普通变量进行此操作。
我曾经写过一篇重要的博客文章讲述这个问题:引发事件的标准模式。
EventHandler handler = MyEvent;
if (handler != null)
    handler(this, EventArgs.Empty);

为了防止在单独的线程上删除事件处理程序时出现问题,我们读取MyEvent的当前值,并仅在该委托非空时调用事件处理程序。
如果引入了来自堆的读取,则编译器/JIT可能会决定再次读取MyEvent而不是使用本地变量,这将引入竞争条件。

很好的解释!这就解释了为什么你不希望CLR引入负载。但是我想不出编译器/JIT可能想要引入存储的地方,你呢? - Eloff
1
@Eloff:一些编译器可能会将“if (cond) { x = y; }”重写为“x = y; if (!cond) { x = old_x; }”,如果它们认为这对于分支预测更好。虽然在单线程场景下没有观察到任何差异,但如果“x”对多个线程可见,这显然会产生不良后果,因此CLR内存模型禁止这样做。 - Bradley Grainger
哎呀,这将使得低锁编程非常困难。引入大量同样如此。因此,规则#5对于理智的多线程非常关键。 - Eloff
如果foo是一个引用一个带有整数字段“bar”的对象,并且我编写“intVar = foo.bar”,在没有内存屏障的情况下,是否保证在引用之后读取该字段?例如,如果foo指向一个bar == 5的对象,并且某些其他代码将另一个对象的bar设置为5并使foo指向该后者对象,那么是否保证foo.bar将看到5而不是新对象的旧值? - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接