C++0x 内存模型和推测加载/存储

标签 c++ concurrency c++11 compiler-optimization memory-model

所以我正在阅读有关即将推出的 C++0x 标准的一部分的内存模型。但是,对于允许编译器执行的一些限制,特别是关于推测性加载和存储的一些限制,我有点困惑。

首先,一些相关的东西:

Hans Boehm's pages about threads and the memory model in C++0x

Boehm, "Threads Cannot be Implemented as a Library"

Boehm and Adve, "Foundations of the C++ Concurrency Memory Model"

Sutter, "Prism: A Principle-Based Sequential Memory Model for Microsoft Native Code Platforms", N2197

Boehm, "Concurrency memory model compiler consequences", N2338

现在,基本思想本质上是“无数据争用程序的顺序一致性”,这似乎是在易于编程和允许编译器和硬件机会进行优化之间的一个不错的折衷。如果不同线程对同一内存位置的两次访问没有排序,则定义为发生数据竞争,其中至少一次存储到内存位置,并且至少一次不是同步操作。这意味着对共享数据的所有读/写访问都必须通过某种同步机制,例如互斥锁或对原子变量的操作(嗯,可以通过宽松的内存顺序对原子变量进行操作仅供专家使用,但默认提供顺序一致性)。

鉴于此,我对普通共享变量上的虚假或推测性加载/存储的限制感到困惑。例如,在 N2338 中我们有示例

switch (y) {
    case 0: x = 17; w = 1; break;
    case 1: x = 17; w = 3; break;
    case 2: w = 9; break;
    case 3: x = 17; w = 1; break;
    case 4: x = 17; w = 3; break;
    case 5: x = 17; w = 9; break;
    default: x = 17; w = 42; break;
}

编译器不允许转换成的

tmp = x; x = 17;
switch (y) {
    case 0: w = 1; break;
    case 1: w = 3; break;
    case 2: x = tmp; w = 9; break;
    case 3: w = 1; break;
    case 4: w = 3; break;
    case 5: w = 9; break;
    default: w = 42; break;
}

因为如果 y == 2 存在对 x 的虚假写入,如果另一个线程同时更新 x,这可能是一个问题。但是,为什么这是一个问题?这是一场数据竞赛,无论如何都是被禁止的;在这种情况下,编译器通过两次写入 x 只会使情况变得更糟,但即使是一次写入也足以进行数据竞争,不是吗? IE。一个合适的 C++0x 程序需要同步对 x 的访问,这样就不会再出现数据争用了,虚假存储也不会成为问题?

我同样对 N2197 中的示例 3.1.3 以及其他一些示例感到困惑,但也许对上述问题的解释也能解释这一点。

编辑:答案:

推测性存储存在问题的原因在于,在上面的 switch 语句示例中,程序员可能选择有条件地获取保护 x 的锁,仅当 y != 2 时。因此,推测性存储可能会引入数据竞争,即原始代码中不存在,因此禁止转换。同样的论点也适用于 N2197 中的示例 3.1.3。

最佳答案

我不熟悉您提到的所有内容,但请注意,在 y==2 的情况下,在代码的第一位,x 根本没有被写入(或读取,就此而言)。在第二段代码中,它被写入了两次。这比只写一次和写两次(至少在现有的线程模型中,例如 pthreads 中)有更大的区别。此外,存储一个根本不会存储的值比仅存储一次与存储两次的区别更大。由于这两个原因,您不希望编译器只是用 tmp = x; 替换无操作。 x = 17; x = tmp;.

假设线程 A 想假设没有其他线程修改 x。希望它被允许期望如果 y 为 2,并且它向 x 写入一个值,然后将其读回,它将取回它已写入的值,这是合理的。但是,如果线程 B 正在同时执行您的第二段代码,那么线程 A 可以写入 x 并稍后读取它,并取回原始值,因为线程 B 在写入“之前”保存并在“之后”恢复它。或者它可以取回 17,因为线程 B 在写入“之后”存储了 17,并且在线程 A 读取之后再次存储了 tmp。线程 A 可以进行它喜欢的任何同步,但这无济于事,因为线程 B 没有同步。它不同步的原因(在 y==2 的情况下)是它没有使用 x。因此,特定代码位是否“使用 x”这一概念对线程模型很重要,这意味着不能允许编译器在“不应该”时更改代码以使用 x。

简而言之,如果允许您提出的转换,引入虚假写入,则永远不可能分析一些代码并得出结论它不会修改 x(或任何其他内存位置)。有许多方便的习惯用法因此是不可能的,例如在没有同步的线程之间共享不可变数据。

所以,虽然我不熟悉 C++0x 对“数据竞争”的定义,但我假设它包含一些允许程序员假设对象未写入的条件,并且这种转换会违反那些条件。我推测如果 y==2,那么你的原始代码,连同并发代码: x = 42; x = 1; z = x 在另一个线程中,未定义为数据竞争。或者至少如果它是一场数据竞争,它不会允许 z 以 17 或 42 结束。

考虑到在这个程序中,y 中的值 2 可能被用来表示,“还有其他线程在运行:不要修改 x,因为我们这里没有同步,所以会引入数据竞争”。也许根本没有同步的原因是,在 y 的所有其他情况下,没有其他线程在运行可以访问 x。在我看来,C++0x 想要支持这样的代码是合理的:

if (single_threaded) {
    x = 17;
} else {
    sendMessageThatSafelySetsXTo(17);
}

很明显,您不希望将其转换为:

tmp = x;
x = 17;
if (!single_threaded) {
    x = tmp;
    sendMessageThatSafelySetsXTo(17);
}

这与您的示例中的转换基本相同,但只有 2 种情况,而不是足以使它看起来像一个很好的代码大小优化。

关于C++0x 内存模型和推测加载/存储,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2001913/

相关文章:

java - ArrayBlockingQueue 超出给定容量

c++ - 避免使用 auto 关键字字面上重复 const 和非常量的代码?

c++ - 如何使用 Native Wifi API C++ 获取 MAC 地址和 channel 信息

Haskell:原子 IO 包装器/惰性?

java - 从桌面运行 JAR 时出现问题,但从命令行或 Eclipse 运行时不出现问题

C++ 代码 : What is wrong in this?

c++ - 如何确保 initializer_list 不为零

c++ - 当类在某些情况下无法运行时,如何禁用类 API?

c++ - 如何增加 std::shared 指针的所有权计数

c++ - 以下 C/C++ 代码是否调用未定义的行为?