实现单个生产者单个消费者的内存屏障



以下来自维基百科的实现:

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

谢谢

如果不使用栅栏,这种重新排序会不会违反代码的正确性?CPU 在使用索引到缓冲区之前不应执行计数增量。在指令重新排序时,CPU 是否不处理数据依赖性?

问得好。

在 C++ 中,除非使用某种形式的内存屏障(原子、互斥等(,否则编译器假定代码是单线程的。在这种情况下,as-if 规则表示编译器可以发出它喜欢的任何代码,前提是整体可观察效果"好像"您的代码是按顺序执行的。

如注释中所述,volatile 不一定会改变这一点,它只是一个实现定义的提示,表明变量可能会在访问之间更改(这与被另一个线程修改不同(。

因此,如果您编写没有内存障碍的多线程代码,则无法保证一个线程中对变量的更改甚至会被另一个线程观察到,因为就编译器而言,其他线程不应该接触相同的内存。

你实际观察到的是未定义的行为。

似乎您的问题是"是否可以在不更改代码行为的情况下重新排序Countbuffer的辅助

请考虑以下代码配置:

int count1 = produceCount++;
buffer[count1 % BUFFER_SIZE] = produceToken();
请注意,代码

的行为与原始代码完全相同:一个从易失变量读取,一个写入易失性变量,读取发生在写入之前,程序的状态是相同的。但是,其他线程将看到有关produceCount增量顺序和buffer修改的不同图片。

编译器和 CPU 都可以在没有内存围栏的情况下执行该转换,因此您需要强制这两个操作按正确的顺序进行。

如果不使用栅栏,这种重新排序会不会违反代码的正确性?

不。你能构建任何可以区分的可移植代码吗?

CPU 在使用索引到缓冲区之前不应执行计数增量。在指令重新排序时,CPU 是否不处理数据依赖性?

为什么不应该呢?所产生的费用的回报是什么?诸如写入组合和推测性获取之类的东西是巨大的优化,禁用它们是不可能的。

如果你认为volatile一个人应该这样做,那根本不是真的。volatile 关键字在 C 或 C++ 中没有定义的线程同步语义。它可能碰巧在某些平台上工作,也可能发生在其他平台上不起作用。在 Java 中,volatile确实定义了线程同步语义,但它们不包括为非易失性的访问提供排序。

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

最新更新