我一直在阅读Full fences
阻止任何类型的指令重新排序或缓存围栏(通过memoryBarrier)
然后我读到了生成“半围栏”的volatile
:
volatile关键字指示编译器生成 在每次从该字段读取时获取栅栏,并释放栅栏 每次写入该字段。
获得围栏
获取栅栏阻止其他读/写之前被移动 围栏;
释放围栏
释放栅栏可防止在移动后移动其他读/写 围栏。
有人可以用简单的英语向我解释这两句话吗?
(围栏在哪里?)
在这里得到一些答案之后 - 我做了一个可以帮助每个人的图画 - 我想。
答案 0 :(得分:13)
您所指的措辞看起来像我经常使用的措辞。规范说明了这一点:
但是,我通常会使用您在问题中引用的措辞,因为我希望将重点放在指令可以移动这一事实上。您引用的措辞和规范是等效的。
我将介绍几个例子。在这些例子中,我将使用一个特殊的符号,它使用↑箭头表示释放栅栏,↓箭头表示获取栅栏。没有其他指令可以通过↑箭头向上浮动或超过↓箭头。把箭头想象成排斥它的一切。
请考虑以下代码。
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
}
这是一个非常简单的例子。请查看我的回答here,了解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中的易失性读取呢?我们将使用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
重要的是要注意,由于我们想模仿易失性读取,因此必须在实际读取之后将Thread.MemoryBarrier
的调用置于。不要陷入认为易失性读取意味着“新读”并且易失性写意味着“提交写入”的陷阱。这不是它的工作原理,它肯定不是规范所描述的。
<强>更新强>
参考图像。
等待!我证实所有的写作都已完成!
和
等待!我正在验证所有消费者都已获得最新消息 值!
这是我正在谈论的陷阱。这些陈述并不完全准确。是的,在硬件级别实现的内存屏障可以同步缓存一致性线,因此上述语句可能在某种程度上准确地说明了所发生的事情。但是,volatile
只会限制指令的移动。该规范说 nothing 关于从内存加载值或将内存存储到内存屏障的位置。
1 当然,Thread.VolatileRead
内置了。你会注意到它的实现与我在这里完全一样。
答案 1 :(得分:1)
从另一个方面开始:
阅读volatile字段时有什么重要意义?之前所有写入该字段的内容都已提交。
写入易失性字段时,重要的是什么?所有以前的读取都已经得到了它们的值。
然后尝试验证在这些情况下获取栅栏和释放栅栏是否有意义。
答案 2 :(得分:1)
为了更容易理解这一点,让我们假设一个可以重新排序的内存模型。
让我们看一个简单的例子。假设这个易变的字段:
volatile int i = 0;
这个读写序列:
1. int a = i;
2. i = 3;
对于指令1,即i
的读取,生成获取栅栏。这意味着对i
的写入的指令2不能与指令1重新排序,因此a
在序列结束时不可能为3。
现在,当然,如果您考虑单个线程,但如果另一个线程对相同的值进行操作(假设a
是全局的),则上述内容没有多大意义:
thread 1 thread 2
a = i; b = a;
i = 3;
在这种情况下,您认为线程2不可能将值3获取到b
(因为它会在赋值之前或之后获得a
的值{{ 1}})。但是,如果对a = i;
的读取和写入进行了重新排序,则i
可能会获得值3.在这种情况下,如果您的程序正确性取决于{{{{}}},则必须使b
为volatile。 1}}不会变成3。
免责声明:以上示例仅供理论之用。除非编译器完全疯了,否则不会去做可能为变量创建“错误”值的重新排序(即使i
不是易变的,b
也不能为3)。
答案 3 :(得分:1)
volatile关键字表示某个字段可能被同时执行的多个线程修改。
为了使程序运行得更快,.NET有时(通常在优化时)会执行智能操作,例如,如果在下一个命令中将更改变量,则不会将变量写入内存:
int i = 0;
//Do some stuff.
i++;
//Do some more stuff.
i--;
//Do other stuff.
这里,编译器会将i的值存储在寄存器中,直到i--;
完成。这节省了从RAM中获取值的少量时间。
如果在线程之间共享i,则线程化时不起作用。例如,你可能有:
//Thread 1:
i = 0; //i is a volatile int shared between threads.
//Do some stuff.
//Wait for Thread 2 to read i.
i++;
//Do some more stuff.
//Wait for Thread 2 to set i = 12.
i--;
//Do other stuff.
//Use i for something like an index.
如果线程1和2将i存储在寄存器中,则更改线程1中的i将不会影响线程2中的i。易失性告诉编译器可以从多个线程访问此变量(i)。因此,它应始终从内存中获取当前值,并将任何更新的值记录到内存中。
另一个例子是SQL表中的值,任何人都可以随时更改该值。正常变量就像查询表一次,然后在本地使用该值,直到完成它为止。挥发性变量就像查询表一样,每次需要时获取/设置最新值,这样每个人都可以访问当前值。
查看volatile (C# Reference)中的示例,因为它提供了如何使用volatile变量的一个很好的示例。
如果您想要更多,请告诉我们。