【问题标题】:Memory barrier in the implementation of single producer single consumer单生产者单消费者实现中的内存屏障
【发布时间】:2016-11-22 02:32:45
【问题描述】:

来自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在指令重排序时是否不考虑数据依赖性?

谢谢

【问题讨论】:

  • @user3286661:这意味着没有内存屏障可以使代码高于定义良好的C++。它对回答没有帮助,它解释了为什么你的问题的前提是有缺陷的。
  • @user3286661 我们不关心性能,对吧?这只是关于正确性。 (因为在sched_yield 上运行的代码的性能会很糟糕。)这必须是伪核心代码或特定于平台的代码。 volatile 如何与内存屏障和线程交互没有可移植的规则。
  • 问题是关于内存屏障的概念。我们不关心性能。
  • @DavidSchwartz 维基百科示例是用伪代码编写的,看起来真的很像 Java。并且在参考资料和链接中多次提到 Java 而没有其他语言。可以安全地假设它是类似 Java 的 volatile ,它提供了额外的保证。
  • @Revolver_Ocelot 是的,Java 的内存模型在这方面和 C++ 是一样的。

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


【解决方案1】:

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

好问题。

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

正如 cmets 中提到的,volatile 不一定会改变这一点,它只是一个实现定义的提示,表明变量可能会在访问之间发生变化(这与被另一个变量修改不同线程)。

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

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

【讨论】:

  • 那么内存栅栏的位置呢。为什么在这两个语句之间?
  • @user3286661内存栅栏的存在阻止了跨栅栏的内存写入重新排序,不仅在当前线程中,而且其他线程观察到。这是重要的部分。它允许将内存更新用作跨线程的信号。
【解决方案2】:

看来,您的问题是 “可以在不改变代码行为的情况下重新排序 Count 和分配给 buffer 吗?”

考虑以下代码转换:

int count1 = produceCount++;
buffer[count1 % BUFFER_SIZE] = produceToken();

请注意,代码的行为与原始代码完全相同:一次从 volatile 变量读取,一次写入 volatile,读取发生在写入之前,程序状态相同。但是,其他线程会看到关于produceCount 增量和buffer 修改顺序的不同图片。

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

【讨论】:

    【解决方案3】:

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

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

    CPU 在用于索引缓冲区之前不应执行 Count 的递增。 CPU在指令重排序时是否不考虑数据依赖性?

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

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

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

    【讨论】:

      猜你喜欢
      • 2017-03-08
      • 2011-10-02
      • 2018-10-16
      • 1970-01-01
      • 1970-01-01
      • 2017-02-24
      • 2015-04-05
      • 2018-08-26
      • 2011-12-16
      相关资源
      最近更新 更多