您提到的措辞看起来像我经常使用的措辞。规范说明了这一点:
- 对易失性字段的读取称为易失性读取。易失性读取具有“获取语义”;也就是说,它保证在指令序列中对内存的任何引用之前发生。
- 对易失性字段的写入称为易失性写入。易失性写入具有“释放语义”;也就是说,它保证发生在指令序列中写指令之前的任何内存引用之后。
但是,我通常使用您在问题中引用的措辞,因为我想将重点放在可以移动指令的事实上。您引用的措辞和规范是等效的。
我将举几个例子。在这些示例中,我将使用一个特殊的符号,使用 ↑ 箭头表示释放栅栏,使用 ↓ 箭头表示获取栅栏。不允许任何其他指令向下飘过 ↑ 箭头或向上飘过 ↓ 箭头。把箭头想象成排斥一切的东西。
考虑以下代码。
static int x = 0;
static int y = 0;
static void Main()
{
x++
y++;
}
重写它以显示各个指令看起来像这样。
static void Main()
{
read x into register1
increment register1
write register1 into x
read y into register1
increment register1
write register1 into y
}
现在,因为在此示例中没有内存屏障,只要执行线程感知的逻辑顺序与物理顺序一致, C# 编译器、JIT 编译器或硬件就可以自由地以多种不同方式对其进行优化。这是一种这样的优化。注意读写是如何被交换的x
。y
static void Main()
{
read y into register1
read x into register2
increment register1
increment register2
write register1 into y
write register2 into x
}
现在这一次将这些变量更改为volatile
. 我将使用我们的箭头符号来标记内存障碍。注意读取和写入的顺序是如何x
保留y
的。这是因为指令不能越过我们的障碍(用↓和↑箭头表示)。现在,这很重要。请注意,指令的增量和写入x
仍允许向下浮动,而读取的指令仍允许y
向上浮动。这仍然有效,因为我们使用的是半栅栏。
static volatile int x = 0;
static volatile int y = 0;
static void Main()
{
read x into register1
↓ // volatile read
read y into register2
↓ // volatile read
increment register1
increment register2
↑ // volatile write
write register1 into x
↑ // volatile write
write register2 into y
}
这是一个非常微不足道的例子。在此处查看我的答案,以了解如何volatile
在双重检查模式中有所作为的重要示例。我使用了与此处相同的箭头符号,以便轻松可视化正在发生的事情。
现在,我们也有了可以使用的Thread.MemoryBarrier
方法。它会生成一个完整的围栏。因此,如果我们使用箭头符号,我们也可以想象它是如何工作的。
考虑这个例子。
static int x = 0;
static int y = 0;
static void Main
{
x++;
Thread.MemoryBarrier();
y++;
}
如果我们要像以前一样显示各个指令,那么它看起来像这样。请注意,现在完全阻止了指令移动。在不影响指令逻辑顺序的情况下,真的没有其他方法可以执行它。
static void Main()
{
read x into register1
increment register1
write register1 into x
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
read y into register1
increment register1
write register1 into y
}
好的,再举一个例子。这次让我们使用VB.NET。VB.NET 没有volatile
关键字。那么我们如何在 VB.NET 中模拟 volatile 读取呢?我们将使用Thread.MemoryBarrier
. 1
Public Function VolatileRead(ByRef address as Integer) as Integer
Dim local = address
Thread.MemoryBarrier()
Return local
End Function
这就是我们的箭头符号的样子。
Public Function VolatileRead(ByRef address as Integer) as Integer
read address into register1
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
return register1
End Function
重要的是要注意,由于我们要模拟 volatile 读取,因此Thread.MemoryBarrier
必须在实际读取之后放置对的调用。不要陷入认为 volatile 读取意味着“新读取”而 volatile 写入意味着“已提交写入”的陷阱。这不是它的工作方式,当然也不是规范所描述的。
更新:
参考图像。
等待!我正在验证所有写入都已完成!
和
等待!我正在验证所有消费者都获得了当前值!
这就是我说的陷阱。这些陈述并不完全准确。是的,在硬件级别实现的内存屏障可能会同步缓存一致性行,因此上述陈述可能对所发生的情况有所准确。但是,volatile
无非是限制指令的移动。该规范没有说明从内存中加载值或将其存储到内存屏障所在位置的内存中。
1当然,Thread.VolatileRead
内置函数已经存在。你会注意到它的实现与我在这里所做的完全一样。