在单一生产者单一消费者的实施中的记忆障碍

时间:2016-07-19 09:23:25

标签: multithreading concurrency operating-system producer-consumer memory-barriers

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是否不处理数据依赖性?

由于

3 个答案:

答案 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确实定义了线程同步语义,但它们不包括提供对非易失性的访问的排序。

但是,内存障碍确实具有明确定义的线程同步语义。我们需要确保在看到数据之前没有线程可以看到数据可用。我们需要确保在线程完成该数据之前不会看到标记数据的线程。