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