在Bjarne Stroustrup编写的C++ Programming Language手册第4版第15.4.2节中,有以下文本:
考虑:整数x = 3;整数y = sqrt(++x);x和y的值可能是什么?显而易见的答案是'' 3和2!''为什么?用常量表达式初始化静态分配的对象是在链接时完成的,所以x变为3。然而,y的初始化器不是常量表达式(sqrt()不是constexpr),所以y直到运行时才初始化。然而,单个翻译单元中静态分配的对象的初始化顺序被很好地定义:它们按照定义顺序初始化(§15.4.1)。因此,y变为2。此参数中的缺陷在于,如果使用多个线程(§5.3.1,§42.2),则每个线程都将执行运行时初始化。没有隐式提供互斥来防止数据争用。然后,一个线程中的sqrt(++x)可能在另一个线程设法递增x之前或之后发生。因此,y的值可以是sqrt(4)或sqrt(5)。
我想知道多个线程如何初始化一个全局变量。如果我有一个std::thread
全局变量在main
函数之前启动线程,这会影响全局变量的初始化吗?我尝试了以下方法:
#include <iostream>
#include <thread>
using namespace std;
extern int x;
extern double y;
struct Foo {
Foo() { cout << "Foo::Foo()" << '\n'; }
};
void threadFunc() {
cout << "x = " << x << '\n';
cout << "y = " << y << '\n';
}
thread t{threadFunc};
int x = 3;
double y = sqrt(double(++x));
Foo z;
int main() {
cout << "x = " << x << '\n';
cout << "y = " << y << '\n';
t.join();
return 0;
}
但是我在多次运行这个程序的时候,没有得到y的值不等于2的结果,而且Foo
的构造函数总是只被调用一次,在测试中,我使用了MSVC Version 17.5.2。
1条答案
按热度按时间mm5n2pyu1#
据我所知,一个变量不可能初始化两次,并且可以保证示例中的初始化顺序严格为
x
-〉t
-〉y
-〉z
(在 sequenced-before 的意义上)。这是因为x
是常量初始化的,因此它将在任何动态初始化和t
之前被初始化,y
和z
都已 * 排序动态初始化 *(假设实现在可能的情况下没有选择用静态初始化替换动态初始化)在同一个翻译单元中定义的顺序。我认为标准在这一点上是非常清楚的,至少从C17开始(参见[basic.start.dynamic]/3.1.1)。“Sequenced-before”也暗示初始化发生在同一个线程中。(这只是因为你所有的变量都有 ordered dynamic initialization 并且定义在同一个翻译单元中!如果不是这样,3.1.1将不适用,并且你不能保证初始化将发生在哪个线程上。)
因此,
t
和y
的初始化将发生在同一线程上,并且y
在t
之后。但是,由t
的初始化启动的线程将与此线程并行运行。因此,
y
的初始化和cout << "y = " << y << '\n';
中y
的访问在线程内部没有任何同步,这两个过程在两个线程中并行发生,因此这是一个数据竞争,导致未定义的行为。出于同样的原因,您也不能保证
y
的初始化不会在threadFunc
从x
读取时写入x
,这是另一个导致未定义行为的数据竞争。我认为“Then,sqrt(++x)in one thread may close before or after the other thread manage to increment x”的说法是不正确的,至少在最近的C版本中是这样。我没有看到任何东西允许在不同线程上交错表达式的单个求值,也没有看到任何东西允许初始化执行两次。正常的 sequenced-before 规则仍然适用。
即使在C11标准中,动态初始化的规则比上面提到的要宽松,我也不知道这是如何应用的。也许这本书是基于C11的早期草案。在C11之前,C标准中没有关于线程的内容,一切都取决于实现如何处理线程。所以这可能是当时的常见行为。
但正如您在上面看到的,无论如何,仍然很容易导致数据竞争,本书从引用部分得出的指导方针仍然适用,应该遵循。