有读屏障和写屏障;获取屏障和释放屏障。还有更多(例如io与内存等)。
这些屏障不是用来控制值的“最新”或“新鲜程度”的,而是用来控制内存访问的相对顺序。
写屏障控制写入顺序。因为与CPU速度相比,对内存的写入速度很慢,所以通常会有一个写请求队列,在它们“真正发生”之前将写入提交到队列中。尽管它们按顺序排队,但在队列内部,写入可能会被重新排序。除非使用写屏障来防止重新排序。
读屏障控制读取顺序。由于预测执行(CPU向前查看并提前从内存中加载)以及写缓冲存在(如果写缓冲中已经有了某个值,CPU将从写缓冲中读取而不是从内存中读取该值-即CPU认为刚刚写入X=5,那么为什么要再次读取,只需查看它是否仍在等待成为5的写缓冲区)。可能会出现读取顺序错误的情况。
这是无论编译器试图如何处理生成的代码都是正确的。即C ++中的“volatile”在这里没有帮助,因为它只告诉编译器输出重新读取“内存”中的值的代码,它不会告诉CPU如何/在哪里读取它(即“内存”在CPU级别上是很多东西)。
因此,读/写屏障用于防止在读/写队列中重新排序的块。
什么样的屏障? -获取和/或释放块。
Acquire-例如read-acquire(x)将把对x的读取添加到读取队列中并刷新队列(实际上不是刷新队列,而是添加一个标记,表示在该读取之前不要重新排序任何内容,这就好像队列被刷新了一样)。因此,稍后(按代码顺序)的读取可以重新排序,但不能在x的读取之前重新排序。
Release-例如write-release(x,5)将首先刷新(或标记)队列,然后将写入请求添加到写入队列中。因此,早期的写操作不会重新排序以在x = 5之后发生,但请注意,稍后的写操作可以在x = 5之前重新排序。
请注意,我将读与获取配对,将写与释放配对,因为这是典型的,但可能有不同的组合。
获取和释放被认为是“半屏障”或“半围栏”,因为它们只阻止重新排序沿着一条路线进行。
完整的屏障(或完整的围栏)应用获取和释放-即没有重新排序。
通常情况下,对于无锁编程或C#或Java中的“volatile”,您��要的是read-acquire和write-release。
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,否则其他线程可能会提前看到 foo->ready 并获取 x/y/z 的错误值。因此,我们在 foo->ready 上使用
write_release
,如上所述,它有效地“刷新”写队列(确保 x、y、z 已经提交),然后将 ready=true 请求添加到队列中。然后再添加 bar=13 请求。请注意,由于我们仅仅使用了 release 屏障(而非 full),bar=13 可能会在 ready 之前被写入。但这没关系!也就是说,我们假设 bar 不会改变共享数据。
现在 threadB() 需要知道当我们说“ready”时,我们确实是指准备好了。因此,我们要使用
read_acquire(foo->ready)
。这个读取操作被添加到读取队列中,然后队列被刷新。请注意,
w = some_global
可能仍然在队列中。因此,foo->ready 可能会在
some_global
之前被读取。但同样的,我们不在乎,因为它不是我们正在小心翼翼处理的重要数据的一部分。
我们关心的是 foo->x/y/z。因此,在获取刷新/标记之后,它们被添加到读取队列中,确保它们在读取 foo->ready 之后被读取。
还要注意,这通常是锁定和解锁互斥体/CriticalSection/etc.使用的确切屏障(即在 lock() 上采用 acquire,在 unlock() 上采用 release)。
因此,
我相当确定这(即获取/释放)正是 MS 文档中所说的 C# “易失性”变量的读/写操作所发生的事情(对于 MS C++ 可选,但这是非标准的)。请参见 http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx,包括“volatile 读取具有“获取语义”,也就是说,它被保证在任何引用它之后的内存引用之前发生...”
我认为 Java 也是一样的,尽管我不是很熟悉。我怀疑它完全一样,因为你通常不需要比读取获取更多的保证。
在你的问题中,你考虑到了相对顺序,但你把顺序弄反了(即“读取的值至少与屏障之前的读取一样新吗?”- 不是,屏障之前的读取无关紧要,重要的是屏障之后的读取,写入同理)。
请注意,正如上面提到的,重排序会发生在读和写操作中,因此仅在一个线程上使用屏障而不在另一个线程上使用屏障将不起作用。即 write-release 没有 read-acquire 是不够的。也就是说,即使你按正确顺序编写它,如果没有与写入屏障配对的读取屏障,它可能以错误的顺序进行读取。
最后,请注意,无锁编程和 CPU 内存架构可能比这更复杂,但坚持