因此,我正在阅读即将到来的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来使情况变得更糟,但是即使一次写入也足以应付数据争夺,不是吗?即一个适当的C++ 0x程序将需要同步对x的访问,在这种情况下,将不再存在数据争用,并且伪存储也不会成为问题吗?
同样,我对N2197中的示例3.1.3和其他一些示例也感到困惑,但是也许对上述问题的解释也可以解释这一点。
编辑:答案:
投机存储成为问题的原因是,在上面的switch语句示例中,程序员可能选择仅在y!= 2时有条件地获取保护x的锁。因此,投机存储可能会引入一个不存在于其中的数据竞争。原始代码,因此禁止进行转换。同样的论点也适用于N2197中的示例3.1.3。
最佳答案
我对您所引用的所有内容都不熟悉,但是请注意,在y == 2的情况下,在代码的第一位中,根本没有写入x(或者说是读取)。在代码的第二位,它被写入了两次。这与只写一次与写两次(至少在现有的线程模型(例如pthread)中)相比,有更大的区别。同样,存储原本不会存储的值与仅存储一次与存储两次相比有更多的不同。由于这两个原因,您不希望编译器仅用tmp = x; x = 17; x = tmp;
代替no-op。
假设线程A要假设没有其他线程修改x。合理地希望它被允许预期,如果y为2,并且它将一个值写入x,然后将其读回,它将取回已写入的值。但是,如果线程B同时执行您的第二位代码,则线程A可以写入x并在以后读取它,并取回原始值,因为线程B在“写入之前”保存了它,并在“写入之后”恢复了它。否则它可能会返回17,因为线程B在“写”之后“存储了” 17,并在“线程”读之后“又存储了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种情况,而不足以使它看起来像是对代码大小的优化。