Wikipedia的以下实施:
volatile unsigned int produceCount = 0, consumeCount = 0;
TokenType buffer[BUFFER_SIZE];
void producer(void) {
while (1) {
while (produceCount - consumeCount == BUFFER_SIZE)
sched_yield(); // buffer is full
buffer[produceCount % BUFFER_SIZE] = produceToken();
// a memory_barrier should go here, see the explanation above
++produceCount;
}
}
void consumer(void) {
while (1) {
while (produceCount - consumeCount == 0)
sched_yield(); // buffer is empty
consumeToken(buffer[consumeCount % BUFFER_SIZE]);
// a memory_barrier should go here, the explanation above still applies
++consumeCount;
}
}
表示在访问缓冲区的行和更新Count
变量的行之间必须使用内存屏障。
这样做是为了防止CPU重新排序围栏上方的指令以及下面的指令。 Count
变量在用于索引缓冲区之前不应递增。
如果没有使用围栏,这种重新排序是否会违反代码的正确性?在用于索引缓冲区之前,CPU不应执行Count
的增量。在指令重新排序时CPU是否不处理数据依赖性?
由于
答案 0 :(得分:3)
如果没有使用围栏,这种重新排序是否会违反代码的正确性?在用于索引缓冲区之前,CPU不应该执行Count的递增。在指令重新排序时CPU是否不处理数据依赖性?
好问题。
在c ++中,除非使用某种形式的内存屏障(原子,互斥等),否则编译器会假定代码是单线程的。在这种情况下,as-if规则表明编译器可以发出它喜欢的任何代码,只要整体可观察到的效果是“好像”。你的代码是按顺序执行的。
正如评论中所提到的,volatile
并不一定会改变这一点,仅仅是一个实现定义的提示,即变量可能在访问之间发生变化(这与不同于被修改通过另一个线程)。
因此,如果您编写没有内存障碍的多线程代码,则无法保证一个线程中的变量更改甚至会被另一个线程观察到,因为只要编译器担心其他线程不应该触及同样的记忆,永远。
您将实际观察到的是未定义的行为。
答案 1 :(得分:2)
看来,您的问题是“可以递增Count
并且可以在不更改代码行为的情况下重新排序buffer
的分配吗?”。
考虑以下代码转换:
int count1 = produceCount++;
buffer[count1 % BUFFER_SIZE] = produceToken();
请注意,代码的行为与原始代码完全相同:一个从volatile变量读取,一个写入volatile,读取在写入之前发生,程序状态相同。但是,其他线程会看到有关produceCount
增量和buffer
修改顺序的不同图片。
编译器和CPU都可以在没有内存防护的情况下进行转换,因此您需要强制执行这两个操作。
答案 2 :(得分:2)
如果没有使用围栏,这种重新排序是否会违反代码的正确性?
不。你能构建任何可以区分的可移植代码吗?
在用于索引缓冲区之前,CPU不应该执行Count的递增。在指令重新排序时CPU是否不处理数据依赖性?
为什么不应该这样?所产生的费用会带来什么回报?像写入组合和推测性提取这样的事情是巨大的优化,禁用它们是不可能的。
如果您认为volatile
单独应该这样做,那根本就不是真的。 volatile
关键字在C或C ++中没有定义的线程同步语义。它可能恰好在某些平台上运行,并且可能不会在其他平台上运行。在Java中,volatile
确实定义了线程同步语义,但它们不包括提供对非易失性的访问的排序。
但是,内存障碍确实具有明确定义的线程同步语义。我们需要确保在看到数据之前没有线程可以看到数据可用。我们需要确保在线程完成该数据之前不会看到标记数据的线程。