Jas*_*aty 56 multithreading volatile memory-barriers
某些语言提供的volatile修饰符被描述为在读取支持变量的内存之前执行"读取内存屏障".
读取存储器屏障通常被描述为一种方法,用于确保CPU在屏障之后执行读取之前执行读取之前所请求的读取.但是,使用此定义,似乎仍然可以读取过时值.换句话说,以特定顺序执行读取似乎并不意味着必须查询主存储器或其他CPU以确保读取的后续值实际上反映了读取屏障时系统中的最新值或随后写入阅读障碍.
因此,volatile是否真的保证读取最新值或者只是(喘气!)读取的值至少与屏障之前的读取一样是最新的?还是其他一些解释?这个答案有什么实际意义?
ton*_*ony 113
有阅读障碍和写作障碍; 获得障碍和释放障碍.还有更多(io vs内存等).
障碍不是控制价值的"最新"价值或"新鲜度".它们用于控制存储器访问的相对顺序.
写屏障控制写入的顺序.由于对内存的写入速度很慢(与CPU的速度相比),因此通常会有一个写入请求队列,其中写入在"真正发生"之前发布.尽管它们按顺序排队,但在队列内部可以重新排序写入.(所以也许'队列'不是最好的名字......)除非你使用写屏障来防止重新排序.
读取障碍控制读取的顺序.由于推测性执行(CPU向前看,并且早期从内存加载)并且由于写入缓冲区的存在(如果存在写入缓冲区,CPU将从写入缓冲区而不是内存中读取值 - 即CPU认为它只是写入X = 5,那么为什么读回,只是看到它仍然在等待成为 5写入缓冲器)读取可能会发生失灵.
无论编译器尝试对生成的代码的顺序做什么都是如此.即C++中的'volatile'在这里没有帮助,因为它只告诉编译器输出代码来重新读取"内存"中的值,它不会告诉CPU如何从哪里读取它(即"内存")是CPU级别的很多东西).
因此,读/写障碍会设置块以防止在读/写队列中重新排序(读取通常不是队列中的那么多,但重新排序的效果是相同的).
什么样的街区? - 获取和/或释放块.
获取 - 例如read-acquire(x)将x的读取添加到读取队列中并刷新队列(不是真正刷新队列,而是添加一个标记,表示在读取之前不对任何内容重新排序,这就好像队列被刷新了).所以稍后(按代码顺序)读取可以重新排序,但不能在读取x之前.
释放 - 例如,写 - 释放(x,5)将首先刷新(或标记)队列,然后将写请求添加到写队列.因此,先前的写入不会在x = 5之后重新排序,但请注意,稍后的写入可以在x = 5之前重新排序.
请注意,我将读取与获取和写入配对发布,因为这是典型的,但可能有不同的组合.
获取和释放被认为是"半障碍"或"半障碍",因为它们只会阻止单向重新排序.
完整屏障(或完整栅栏)同时适用于获取和释放 - 即不重新排序.
通常对于无锁编程,或C#或java'volatile',您想要/需要的是读取和写入释放.
即
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_releaseon 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 /等的完全相同的障碍.(即获取锁定(),释放解锁()).
所以,
我很确定这个(即获取/发布)正是MS文档所说的在C#中读取/写入"volatile"变量(对于MS C++而言,这是非标准的).请参阅http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx,包括"易失性读取具有获取语义";也就是说,它保证在任何对内存的引用之前发生之后发生......"
我认为 java是一样的,虽然我并不熟悉.我怀疑它完全相同,因为你通常不需要比读取 - 获取/写入 - 释放更多的保证.
在你的问题中,当你认为它实际上是关于相对顺序的时候,你是在正确的轨道上 - 你只是向后排序(即"读取的值至少与屏障之前的读数一样是最新的? " - 不,在屏障之前读取是不重要的,它在屏障之后读取,保证是后来的,反之亦然."
请注意,如上所述,重新排序在读取和写入时都会发生,因此仅在一个线程上使用屏障而不在另一个线程上使用屏障将不起作用.即没有读取获取,写入释放是不够的.即使你以正确的顺序写它,如果你没有使用读屏障来配合写屏障,它可能会以错误的顺序读取.
最后,请注意,无锁编程和CPU内存架构实际上可能比这更复杂,但坚持获取/发布将使您走得更远.
volatile在大多数编程语言中,并不意味着真正的CPU读取内存障碍,而是要求编译器不要通过寄存器中的缓存来优化读取.这意味着读取进程/线程将获得"最终"的值.一种常见的技术是声明一个布尔volatile标志,在一个信号处理程序中设置并在主程序循环中检查.
相比之下,CPU内存屏障可以通过CPU指令直接提供,也可以与某些汇编程序助记符(例如lockx86中的前缀)一起提供,例如在与硬件设备通信时使用,其中对内存映射IO寄存器的读写顺序很重要或者在多处理环境中同步内存访问.
回答你的问题 - 不,内存屏障不保证"最新"值,但保证内存访问操作的顺序.这对于无锁编程至关重要.
这是CPU内存屏障上的一个引子.