我如何理解阅读内存障碍和易失性
某些语言提供了一个volatile
修饰符,它被描述为在读取支持variables的内存之前执行“读取内存障碍”。
读取内存屏障通常被描述为确保CPU在屏障之前执行读取之前所请求的读取,然后在屏障之后执行读取请求的方式。 但是,使用这个定义,似乎仍然可以读取陈旧的价值。 换句话说,以某一顺序执行读取似乎并不意味着必须查阅主存储器或其他CPU以确保读取的后续值实际上反映了在读取屏障时系统中的最新值或者在阅读障碍。
那么,volatile是否确实保证读取的是最新的值,或者只是读取的值至less与屏障之前的读取值一样是最新的(gasp!)。 还是其他一些解释? 这个答案的实际含义是什么?
有阅读障碍和障碍; 获得障碍并释放障碍。 还有更多(io vs内存等)。
障碍不在于控制价值的“最新”价值或“新鲜度”。 他们在那里控制内存访问的相对顺序。
写障碍控制写入顺序。 由于对内存的写入速度较慢(与CPU的速度相比),因此通常会有一个写入请求队列,在这些写入请求队列中,写入操作会在“真正发生”之前发布。 虽然它们按顺序排队,但是在队列中写入可能会被重新sorting。 (所以也许'队列'是不是最好的名字…)除非你使用写屏障,以防止重新sorting。
读取障碍控制读取的顺序。 由于推测性执行(CPU超前并从内存中提前加载),并且由于写缓冲区的存在(如果CPU存在,它将从写入缓冲区而不是内存中读取一个值,即CPU认为它只写了X = 5,那么为什么读回来,只是看到它仍然等待成为写入缓冲区中的5)读取可能发生不按顺序。
无论编译器如何处理生成的代码的顺序,情况都是如此。 即C ++中的'volatile'在这里没有帮助,因为它只是告诉编译器输出代码重新读取“内存”中的值,它并不告诉CPU如何/从何处读取它(即“内存”在CPU层面是很多东西)。
所以读/写屏障阻止了在读/写队列中的重新sorting(读取通常不是很多队列,但是重新sorting的效果是相同的)。
什么types的块? – 获取和/或释放块。
获取 – 例如read-acquire(x)会将x的读取添加到读取队列中, 并刷新队列 (不是真正地刷新队列,而是添加一个标记,在读取之前不要重新sorting任何东西,就好像队列被刷新)。 所以稍后(按代码顺序)读取可以重新sorting,但不能在读取x之前。
释放 – 例如,写入释放(x,5)将首先刷新(或标记)队列,然后将写入请求添加到写入队列。 所以之前的写入不会在x = 5之后发生重新sorting,但是请注意,以后的写入可以在x = 5之前重新sorting。
请注意,我将阅读与获取和发布配对,因为这是典型的,但不同的组合是可能的。
获得和释放被认为是“半壁垒”或“半壁垒”,因为它们只能阻止重新sorting。
完全屏障(或完整屏障)适用于获取和发布 – 即不重新sorting。
通常对于无locking编程,或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到最后,否则其他线程可能会提前看到foo-> ready,并得到x / y / z的错误值。 因此,我们在foo-> ready上使用write_release
,如上所述,这有效地“刷新”写入队列(确保x,y,z已被提交),然后将ready = true请求添加到队列中。 然后添加bar = 13请求。 请注意,因为我们只是使用了一个释放屏障(不是完整的),所以在准备好之前,可能会写入13。 但我们不在乎! 即我们假设酒吧不改变共享数据。
现在threadB()需要知道,当我们说'准备好',我们真的意味着准备。 所以我们做一个read_acquire(foo->ready)
。 这个读取被添加到读取队列,然后队列被刷新。 请注意, w = some_global
也可能仍然在队列中。 所以foo-> ready可以在 some_global
之前 some_global
。 但是,我们也不在乎,因为这不是我们如此谨慎的重要数据的一部分。 我们所关心的是foo-> x / y / z。 所以它们在获取flush / marker之后被添加到读取队列中,保证只有在读取foo-> ready之后才能读取它们。
还要注意,这通常是用于locking和解锁互斥/ CriticalSection /等的完全相同的屏障。 (即获取锁(),释放解锁())。
所以,
-
我很确定这(即获取/释放)正是MS文档所说的在C#中(和可选的MS C ++,但这是非标准的)读/写'volatile'variables。 请参阅http://msdn.microsoft.com/en-us/library/aa645755(VS.71).aspx,其中包括“一个易失性读取”获取语义“;也就是说,它是保证发生之前任何引用的内存发生在…之后“
-
我认为 Java是一样的,虽然我不是很熟悉。 我怀疑它是完全一样的,因为你通常不需要比read-acquire / write-release更多的保证。
-
在你的问题中,当你认为这完全是关于相对秩序的时候,你是在正确的轨道上 – 你只是把顺序倒退了(即“读取的值至less和屏障之前的读数一样是最新的? “ – 不,在屏障前读取不重要,其读取后保证为AFTER,反之写入)。
-
请注意,如上所述,重新sorting发生在读取和写入,所以只有在一个线程上使用屏障而不是另一个线程是行不通的。 即没有读取获取,写入释放是不够的。 即使你按照正确的顺序编写,如果你没有使用读取障碍来解决写入障碍,也可以按照错误的顺序读取它。
-
最后,请注意,无锁编程和CPU内存体系结构实际上可能比这更复杂,但坚持获取/释放将使您获得相当的效果。
在大多数编程语言中是volatile
,并不意味着真正的CPU读取内存屏障,而是指令编译器不通过寄存器中的高速caching来优化读取。 这意味着读取过程/线程将得到“最终”的值。 常用的技术是在信号处理程序中声明布尔volatile
标志,并在主程序循环中进行检查。
相比之下,CPU内存屏障直接通过CPU指令提供,或者通过某些汇编助记符(如x86中的lock
前缀)提供,例如在与硬件设备通信时使用,其中对存储器映射IO寄存器的读写顺序很重要或在多处理环境中同步内存访问。
要回答你的问题 – 不,内存屏障不保证“最新”的价值,但保证内存访问操作的顺序 。 例如在无锁编程中,这是至关重要的。
这是CPU内存屏障的一个引脚。