关于volatile和Memory-Barrier我不明白的是

msl*_*lot 4 c# multithreading volatile memory-model memory-barriers

循环提升易失性读数

我已经阅读过很多地方,一个volatile变量不能从循环中提升或者如果,但我找不到这提到C#规范中的任何地方.这是隐藏的功能吗?

所有写入在C#中都是易失的

这是否意味着所有写入都没有相同的属性,就像使用volatile关键字一样?例如,C#中的普通写入具有发布语义?并且所有写入都会刷新处理器的存储缓冲区?

释放语义

这是一种正式的方式,说明当完成易失性写入时处理器的存储缓冲区被清空了吗?

获取语​​义

这是一种正式的说法是不应该将变量加载到寄存器中,而是每次从内存中获取它吗?

本文中,Igoro谈到"线程缓存".我完全明白这是想象的,但他实际上指的是:

  1. 处理器存储缓冲区
  2. 将变量加载到寄存器中而不是每次从内存中取出
  3. 某种处理器缓存(这是L1和L2等)

或者这只是我的想象力?

延迟写作

我读过许多写作可以延迟的地方.这是因为重新排序和存储缓冲区吗?

Memory.Barrier

我知道副作用是在JIT将IL转换为asm时调用"lock or",这就是为什么Memory.Barrier可以解决fx这个例子中对主内存(在while循环中)的延迟写入的原因:

static void Main()
{
  bool complete = false; 
  var t = new Thread (() =>
  {
    bool toggle = false;
    while (!complete) toggle = !toggle;
  });
  t.Start();
  Thread.Sleep (1000);
  complete = true;
  t.Join();        // Blocks indefinitely
}
Run Code Online (Sandbox Code Playgroud)

但情况总是这样吗?对Memory.Barrier的调用是否总是刷新存储缓冲区,将更新的值提取到处理器缓存中?我知道完整的变量不会被提升到寄存器中,而是每次都从处理器缓存中提取,但由于对Memory.Barrier的调用,处理器缓存会更新.

我在这里的冰上,还是我对volatile和Memory.Barrier的某种理解?

dca*_*tro 12

那是满口的..

我将从你的一些问题开始,并更新我的答案.


循环提升挥发性

我已经阅读过很多地方,一个volatile变量不能从循环中提升或者如果,但我找不到这提到C#规范中的任何地方.这是隐藏的功能吗?

MSDN说"声明为volatile的字段不受编译器优化的约束,这些优化假定单个线程访问".这是一种广泛的陈述,但它包括从循环中提升或"提升"变量.


所有写入在C#中都是易失的

这是否意味着所有写入都没有相同的属性,就像使用volatile关键字一样?例如,C#中的普通写入具有发布语义?并且所有写入都会刷新处理器的存储缓冲区?

经常写是挥发.它们确实具有释放语义,但它们不会刷新CPU的写缓冲区.至少,不是根据规范.

来自Joe Duffy的CLR 2.0内存模型

规则2:所有商店都有发布语义,即没有加载或商店可能会在一个商店之后移动.

我已经阅读了一些文章,说明所有写入在C#中都是易失性的(就像你链接到的那样),但这是一个常见的误解.从马的嘴里(理论与实践中的C#记忆模型,第2部分):

因此,作者可能会说,"在.NET 2.0内存模型中,所有写入都是易失性的 - 甚至是那些非易失性字段."(...)ECMA C#规范不保证这种行为,因此,可能不会在.NET Framework的未来版本和未来的体系结构中保留(事实上,在ARM上的.NET Framework 4.5中并不成立).


释放语义

这是一种正式的方式,说明当完成易失性写入时处理器的存储缓冲区被清空了吗?

不,这是两件不同的事情.如果指令具有"释放语义",则不会在所述指令下方移动存储/加载指令.该定义没有说明有关刷写写缓冲区的内容.它只涉及指令重新排序.


延迟写作

我读过许多写作可以延迟的地方.这是因为重新排序和存储缓冲区吗?

是.写入指令可以由编译器,抖动或CPU本身进行延迟/重新排序.


因此,volatile写入有两个属性:释放语义和存储缓冲区刷新.

有点.我更喜欢这样想:

volatile关键字的C#规范保证了一个属性:读取具有获取语义,写入具有释放语义.这是通过发射必要的释放/获取围栏来完成的.

实际的微软的C#实现增加了另一个特性:读取将是新鲜的,并写入会立即刷新到内存,让用户看到它们的其他处理器.为了实现这一点,编译器发出一个OpCodes.Volatile,并且抖动选择它并告诉处理器不要将这个变量存储在它的寄存器上.

这意味着不保证即时性的不同C#实现将是完全有效的实现.


记忆障碍

bool complete = false; 
var t = new Thread (() =>
{
    bool toggle = false;
    while (!complete) toggle = !toggle;
});
t.Start();
Thread.Sleep(1000);
complete = true;
t.Join();     // blocks
Run Code Online (Sandbox Code Playgroud)

但情况总是这样吗?对Memory.Barrier的调用是否总是刷新存储缓冲区,将更新的值提取到处理器缓存中?

这里有一个提示:尝试将自己从诸如刷新存储缓冲区或直接从内存中读取等概念中抽象出来.记忆障碍(或全围栏)的概念与前两个概念无关.

内存屏障只有一个目的:确保栅栏下方的存储/加载指令不会移动到栅栏上方,反之亦然.如果 C#Thread.MemoryBarrier碰巧刷新挂起的写入,你应该把它看作副作用,而不是主要的意图.

现在,让我们谈谈这一点.您发布的代码(在发布模式下编译并在没有调试器的情况下运行时阻塞)可以通过在while块内的任何位置引入完整的栅栏来解决.为什么?让我们首先展开循环.这是前几次迭代的样子:

if(complete) return;
toggle = !toggle;

if(complete) return;
toggle = !toggle;

if(complete) return;
toggle = !toggle;
...
Run Code Online (Sandbox Code Playgroud)

因为complete没有标记为volatile并且没有围栏,所以允许编译器 cpu移动complete字段的读取.事实上,CLR的内存模型(参见规则6)允许在合并相邻负载时删除负载(!).所以,这可能发生:

if(complete) return;
toggle = !toggle;
toggle = !toggle;
toggle = !toggle;
...
Run Code Online (Sandbox Code Playgroud)

请注意,这在逻辑上等同于提升循环的读出,这正是编译器可能做的事情.

通过在之前或之后引入全栅栏toggle = !toggle,您将阻止编译器移动读取并将它们合并在一起.

if(complete) return;
toggle = !toggle;
#FENCE
if(complete) return;
toggle = !toggle;
#FENCE
if(complete) return;
toggle = !toggle;
#FENCE
...
Run Code Online (Sandbox Code Playgroud)

总之,解决这些问题的关键是确保指令以正确的顺序执行.它与其他处理器看到一个处理器的写入所需的时间无关.