【问题标题】:Understanding CLR 2.0 Memory Model了解 CLR 2.0 内存模型
【发布时间】:2011-02-25 20:12:58
【问题描述】:

Joe Duffy,给了6 rules that describe the CLR 2.0+ memory model(它是实际的实现,不是任何 ECMA 标准)我正在写下我试图解决这个问题的尝试,主要是作为一种橡胶回避的方式,但如果我的逻辑有误,至少这里有人能在它让我伤心之前抓住它。

  • 规则 1:负载之间的数据依赖性 并且商店永远不会受到侵犯。
  • 规则 2:所有商店都有发布语义, 即没有负载或存储可以移动 一。
  • 规则 3:所有易失性负载均 获取,即没有加载或存储可能 移到前一个。
  • 规则 4:无负载和 商店可能会跨越一个完整的障碍 (例如 Thread.MemoryBarrier,锁 获取,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

看这个,看来load 0 可以上移到load y 之前,但是store 可能根本不会重新排序。因此,如果一个线程看到 z == 0,那么它也会看到 x == y。

如果 y 是 volatile,则加载 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 之外的所有内容。任何人都想启发我(或纠正我或在上述任何内容中添加一些内容?)

【问题讨论】:

    标签: c# .net multithreading memory lock-free


    【解决方案1】:

    Joe Duffy 在Concurrent Programming on Windows 的 pp517-18 上讨论了规则 5:

    作为一个负载可能是什么时候的例子 介绍,考虑这段代码:

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

    如果初始之间的时间段 将 mo.field 读入变量 f 和 随后在 Console.WriteLine 足够长,一个 编译器可能会决定它会更多 有效地重读 mo.field 两次。 ...如果这样做的话,这将是一个问题 mo 是一个堆对象,线程是 同时写入 mo.field。这 if-block 可能包含假设 读入 f 的值保持为 0,并且 引入读取可能会中断 这个假设。此外 禁止这个 volatile 变量,.NET 内存模型 禁止它用于普通变量 也指 GC 堆内存。

    blogged about one important place where this matters:引发事件的标准模式。

    EventHandler handler = MyEvent;
    if (handler != null)
        handler(this, EventArgs.Empty);
    

    为了防止在单独线程上删除事件处理程序时出现问题,我们读取MyEvent 的当前值,并且仅在该委托为非空时调用事件处理程序。

    如果可以引入从堆中读取,编译器/JIT 可能会决定最好再次读取 MyEvent,而不是使用会引入竞争条件的本地。

    【讨论】:

    • 很好的解释!这就解释了为什么您不希望 CLR 引入负载。不过,我想不出编译器/JIT 可能想要引入商店的地方,你可以吗?
    • @Eloff:一些编译器可能会将“if (cond) { x = y; }”重写为“x = y; if (!cond) { x = old_x; }”,如果他们认为会的话更适合分支预测。虽然在单线程场景中没有可观察到的差异,但如果 'x' 对多个线程可见,这显然会产生不良后果,因此 CLR 内存模型禁止这样做。
    • 哎呀,这将使低锁编程变得非常不可能。同上引入负载。所以规则 #5 对于健全的多线程非常关键。
    • 如果 foo 是对具有整数字段“bar”的对象的引用,并且我编码“intVar = foo.bar”,是否可以保证在没有内存屏障的情况下该字段将是参考后阅读?例如,如果 foo 指向一个 bar==5 的对象,而其他一些代码将其他对象的 bar 设置为 5 并使 foo 指向后一个对象,是否保证 foo.bar 将看到 5 而不是新对象的旧值?
    猜你喜欢
    • 2011-06-03
    • 2020-12-13
    • 1970-01-01
    • 2013-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-04
    相关资源
    最近更新 更多