C ++ 0x内存模型和推测加载/存储
所以我正在阅读即将到来的C ++ 0x标准中的内存模型。 不过,我对编译器允许执行的一些限制有些困惑,特别是关于投机加载和存储的限制。
首先,一些相关的东西:
Hans Boehm关于线程和C ++ 0x中的内存模型的页面
Boehm,“线程不能作为一个库来实现”
Boehm和Adve,“C ++并发内存模型的基础”
Sutter,“棱镜:一种基于原则的序列存储器模型,适用于Microsoft本地代码平台”,N2197
Boehm,“并发内存模型编译器后果”,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使情况变得更糟,但即使是单次写入也足够用于数据竞争,不是吗? 也就是说一个合适的C ++ 0x程序需要同步对x的访问,在这种情况下,不再有数据竞争,并且虚假存储也不会成为问题?
我对N2197中的Example 3.1.3以及其他一些例子也有类似的困惑,但是对上述问题的解释也可以解释这一点。
编辑:答案:
投机商店是一个问题的原因是,在上面的switch语句示例中,程序员可能选择有条件地获取锁保护x,只有当y!= 2时。因此,投机商店可能会引入一个数据竞争原始代码和转换因此被禁止。 同样的论点也适用于N2197中的例3.1.3。
我并不熟悉你引用的所有东西,但请注意,在y == 2的情况下,在第一个代码中,x根本不写入(或读取)。 在第二位代码中,它被写入两次。 这不仅仅是写一次而写两次(至少,它是在诸如pthread之类的现有线程模型中)。 另外,存储一个根本不存储的值比仅存储一次与存储两次更重要。 由于这两个原因,你不希望编译器用tmp = x; x = 17; x = tmp;
替换no-op tmp = x; x = 17; x = tmp;
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
x = 42; x = 1; z = x
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);
}
这与您的示例中的转换基本相同,但只有两种情况,而不是足以使它看起来像一个很好的代码大小优化。
如果y==2
,而另一个线程修改或读取x
,原始示例中的竞争条件如何? 这个线程永远不会触及x
,所以其他线程可以自由地进行操作。
但是对于重新排序的版本,我们的线程会修改x
,如果只是暂时的,所以如果另一个线程也处理它,我们现在有一个竞争条件,以前没有。