所以我正在阅读有关即将推出的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"
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写入两次来使情况更糟,但即使单次写入也足以进行数据竞争,不是吗?即一个正确的C ++ 0x程序需要同步访问x,在这种情况下,不再有数据竞争,虚假存储也不会有问题吗?
我同样对N2197中的例3.1.3以及其他一些例子感到困惑,但也许对上述问题的解释也可以解释这一点。
编辑:答案:
推测商店存在问题的原因在于,在上面的switch语句示例中,程序员可能已选择仅在y!= 2时有条件地获取锁定保护x。因此,推测商店可能会引入数据竞争在原始代码中没有,因此禁止转换。同样的论点也适用于N2197中的例3.1.3。
答案 0 :(得分:8)
我不熟悉你提到的所有内容,但请注意,在y == 2的情况下,在代码的第一位,根本没有写入x(或者就此而言读取)。在第二位代码中,它被写入两次。这与仅写一次与写两次(至少在现有的线程模型,如pthreads)之间的区别更大。此外,存储一个本来不存储的值与仅存储一次而不是存储两次相比更有区别。出于这两个原因,您不希望编译器只使用tmp = x; x = 17; x = tmp;
替换no-op。
假设线程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种情况,而不是足以让它看起来像一个良好的代码大小优化。
答案 1 :(得分:5)
如果y==2
,另一个线程修改或读取x
,原始样本中的竞争条件如何?该线程永远不会触及x
,因此其他线程可以自由地执行此操作。
但是对于重新排序的版本,我们的线程修改x
,如果只是暂时的,那么如果另一个线程也操纵它,我们现在有一个竞争条件,之前没有。