有读屏障和写屏障;获取障碍和释放障碍。还有更多(io 与内存等)。
不存在控制“最新”价值或“新鲜度”价值的障碍。它们用于控制内存访问的相对顺序。
写屏障控制写的顺序。因为写入内存很慢(与 CPU 的速度相比),通常有一个写入请求队列,在写入“真正发生”之前发布。尽管它们是按顺序排队的,但在队列内部,写入可能会重新排序。(所以也许'队列'不是最好的名字......)除非你使用写障碍来防止重新排序。
读取屏障控制读取的顺序。由于推测性执行(CPU 提前查看并提前从内存中加载)以及写入缓冲区的存在(如果存在,CPU 将从写入缓冲区而不是内存中读取一个值 - 即 CPU 认为它只是写了 X = 5,那为什么要读回来,只是看到它还在写缓冲区中等待变为5)读取可能发生乱序。
无论编译器尝试对生成代码的顺序做什么,这都是正确的。即 C++ 中的 'volatile' 在这里没有帮助,因为它只告诉编译器输出代码以从“内存”重新读取值,它不会告诉 CPU 如何/从哪里读取它(即“内存”在 CPU 级别有很多东西)。
所以读/写屏障设置块以防止在读/写队列中重新排序(读取通常不是队列,但重新排序效果是相同的)。
什么样的积木?- 获取和/或释放块。
Acquire - 例如 read-acquire(x) 会将 x 的读取添加到读取队列中并刷新队列(不是真正刷新队列,而是添加一个标记,表示在读取之前不要重新排序任何内容,就像队列被刷新)。所以稍后(按代码顺序)读取可以重新排序,但不能在读取 x 之前重新排序。
释放 - 例如 write-release(x, 5) 将首先刷新(或标记)队列,然后将写入请求添加到写入队列。因此,较早的写入不会在 x = 5 之后重新排序,但请注意,稍后的写入可以在 x = 5 之前重新排序。
请注意,我将读取与获取和写入与释放配对,因为这是典型的,但不同的组合是可能的。
获取和释放被认为是“半壁垒”或“半栅栏”,因为它们只会阻止重新排序以一种方式进行。
一个完整的屏障(或完整的栅栏)同时应用了获取和释放——即没有重新排序。
通常对于无锁编程或 C# 或 java 'volatile',您想要/需要的是读取-获取和写入-释放。
IE
void threadA()
{
foo->x = 10;
foo->y = 11;
foo->z = 12;
write_release(foo->ready, true);
bar = 13;
}
void threadB()
{
w = some_global;
ready = read_acquire(foo->ready);
if (ready)
{
q = w * foo->x * foo->y * foo->z;
}
else
calculate_pi();
}
所以,首先,这是编写线程的一种不好的方式。锁会更安全。但只是为了说明障碍......
在 threadA() 写完 foo 之后,它需要写 foo->ready LAST,真的是最后一个,否则其他线程可能会提前看到 foo->ready 并得到错误的 x/y/z 值。所以我们使用write_release
on foo->ready ,如上所述,它有效地“刷新”写队列(确保 x,y,z 被提交)然后将 ready=true 请求添加到队列中。然后添加 bar=13 请求。请注意,由于我们只是使用了释放屏障(不是完整的),所以 bar=13 可能在准备好之前就被写入了。但我们不在乎!即我们假设 bar 没有改变共享数据。
现在 threadB() 需要知道,当我们说“准备好”时,我们真正的意思是准备好了。所以我们做一个read_acquire(foo->ready)
. 此读取被添加到读取队列中,然后队列被刷新。请注意,w = some_global
也可能仍在队列中。所以 foo->ready之前 some_global
可能会被读取。但同样,我们不在乎,因为它不是我们如此小心的重要数据的一部分。我们关心的是 foo->x/y/z。所以它们在获取刷新/标记之后被添加到读取队列中,保证它们只有在读取 foo->ready 之后才被读取。
另请注意,这通常与用于锁定和解锁互斥锁/CriticalSection/等的屏障完全相同。(即在 lock() 上获取,在 unlock() 上释放)。
所以,
我很确定这(即获取/释放)正是 MS 文档所说的在 C# 中读/写“易失性”变量时发生的情况(对于 MS C++ 也是可选的,但这是非标准的)。请参阅http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx,包括“易失性读取具有“获取语义”;也就是说,它保证在对内存的任何引用之前发生发生在它之后……”
我认为java是一样的,虽然我不是很熟悉。我怀疑它完全一样,因为您通常不需要比读取-获取/写入-释放更多的保证。
在您的问题中,当您认为这实际上与相对顺序有关时,您走在正确的轨道上-您只是将顺序倒过来(即“读取的值至少与障碍之前的读取值一样最新? “ - 不,在屏障之前读取并不重要,它在屏障之后读取,保证在之后,反之亦然。
请注意,如上所述,重新排序发生在读取和写入上,因此仅在一个线程上使用屏障而不在另一个线程上将不起作用。即没有读获取,写发布是不够的。即,即使您以正确的顺序编写它,如果您不使用读屏障与写屏障一起使用,它也可能以错误的顺序读取。
最后,请注意,无锁编程和 CPU 内存架构实际上可能比这复杂得多,但坚持使用获取/释放将使您走得更远。