C# 内存模型:防止存储释放和加载获取重新排序

C# 内存模型:防止存储释放和加载获取重新排序,c#,.net,performance,volatile,memory-model,C#,.net,Performance,Volatile,Memory Model,众所周知,与Java的volatile不同,.NET的volatile允许对volatile写入进行重新排序,并从另一个位置执行以下volatile读取。当出现问题时,建议在它们之间放置MemoryBarier,或将互锁。可以使用Exchange代替volatile write 它可以工作,但是当在高度优化的无锁代码中使用时,MemoryBarier可能会成为性能杀手 我考虑了一下,想出了一个主意。我想有人告诉我我是否走对了路 因此,想法如下: 我们希望防止在这两个访问之间重新排序: vola

众所周知,与Java的volatile不同,.NET的volatile允许对volatile写入进行重新排序,并从另一个位置执行以下volatile读取。当出现问题时,建议在它们之间放置
MemoryBarier
,或将
互锁。可以使用Exchange
代替volatile write

它可以工作,但是当在高度优化的无锁代码中使用时,
MemoryBarier
可能会成为性能杀手

我考虑了一下,想出了一个主意。我想有人告诉我我是否走对了路

因此,想法如下:

我们希望防止在这两个访问之间重新排序:

 volatile1 write

 volatile2 read
从.NET MM我们知道:

 1) writes to a variable cannot be reordered with  a  following read from 
    the same variable
 2) no volatile accesses can be eliminated
 3) no memory accesses can be reordered with a previous volatile read 
为了防止写入和读取之间出现不必要的重新排序,我们从刚刚写入的变量中引入了一个伪volatile read:

 A) volatile1 write
 B) volatile1 read [to a visible (accessible | potentially shared) location]
 C) volatile2 read
在这种情况下,B不能用A重新排序,因为它们都访问相同的变量, C不能用B重新排序,因为两个易失性读取不能相互重新排序,而传递性C不能用A重新排序

问题是:


我说得对吗?在这种情况下,虚拟易失性读取可以用作轻型内存屏障吗?

编辑我从C规范中得出的结论是错误的,见下文结束编辑

我当然不是“授权”的人,但我认为你没有正确理解记忆模型

引用C#规范第§10.10节执行令第105页第三个要点:

对于易失性读写,保留副作用的顺序

易失性读写被定义为“副作用”,本段声明保留副作用的顺序

所以我的理解是,你的整个问题都是基于一个错误的假设:不稳定的读写不能被重新排序

我想你会对这样一个事实感到困惑,即对于非易失性内存操作,易失性读写仅仅是半个围栏

编辑本文:正好相反,并支持您的断言,即易失性读取可以超过不相关的易失性写入。建议的解决方案是在重要的地方引入内存载体

下面Daniel的评论还指出,CLI规范比C#规范更具体,并允许这种重新排序


现在我发现我上面引用的C#规范令人困惑!但是在x86上,同样的指令用于易失性内存访问和常规内存访问,因此它们也会面临同样的半篱笆重排序问题,这是完全有道理的结束编辑

这里我将使用箭头符号来概念化内存屏障。我使用向上箭头↑ 还有一个向下的箭头↓ 分别表示易失性写入和读取。可以将箭头视为推开任何其他读写操作。因此,没有其他内存访问可以越过箭头,但它们可以越过尾部

考虑你的第一个例子。这就是它的概念化方式

↑          
volatile1 write  // A
volatile2 read   // B
↓
因此,我们可以清楚地看到,读和写可以切换位置。你说得对

现在考虑一下你的第二个例子。您声称引入虚拟读取将防止
a
的写入和
B
的读取被交换

↑          
volatile1 write  // A
volatile1 read   // A
↓
volatile2 read   // B
↓
我们可以看到,
B
A
的伪读阻止上浮。我们还可以看到,
A
的读取不能向下浮动,因为根据推断,这与
B
A
之前向上移动相同。但是,请注意,我们没有↑ 防止写入
A
时向下浮动的箭头(请记住,它仍然可以越过箭头的尾部)。因此,至少从理论上讲,注入
a
的虚拟读取不会阻止
a
的原始写入和
B
的读取被交换,因为对
a
的写入仍然可以向下移动

我必须认真思考这个场景。有一件事我思考了很长一段时间,那就是对
a
的读写是否是串联在一起的。如果是这样的话,那么这将阻止对
A
的写入向下移动,因为它必须带着读,我们已经说过,读被阻止了。因此,如果你坚持这一学派的观点,那么你的解决方案可能会奏效。但是,我再次阅读了规范,我没有看到关于对同一变量的易失性访问的任何特别提及。显然,线程必须以与原始程序序列(规范中提到的)逻辑一致的方式执行。但是,我可以想象编译器或硬件如何优化(或重新排序)对
A
的串联访问,并且仍然得到相同的结果。因此,我只需在这里谨慎地站在一边,并假设对
A
的写入可以向下移动。记住,易失性读取并不意味着“从主存重新读取”。对
A
的写入可以缓存在寄存器中,然后读取来自该寄存器,将实际写入延迟到稍后的时间。据我所知,易变语义并不能阻止这种情况

正确的解决方案是在访问之间调用
Thread.MemoryBarrier
。您可以看到这是如何用箭头符号概念化的

↑          
volatile1 write       // A
↑
Thread.MemoryBarrier
↓
volatile2 read        // B
↓
现在您可以看到,读操作不允许上浮,写操作不允许下浮,从而阻止交换



您可以使用这个箭头符号看到我的其他一些记忆障碍答案,仅举几个例子。

请允许我不同意Brian Gideon接受的答案

奥马里奥您的问题解决方案