C++中全局变量的初始化可以在多个线程上进行吗?

aydmsdu9  于 2023-03-20  发布在  其他
关注(0)|答案(1)|浏览(137)

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。

mm5n2pyu

mm5n2pyu1#

据我所知,一个变量不可能初始化两次,并且可以保证示例中的初始化顺序严格为x-〉t-〉y-〉z(在 sequenced-before 的意义上)。这是因为x是常量初始化的,因此它将在任何动态初始化和t之前被初始化,yz都已 * 排序动态初始化 *(假设实现在可能的情况下没有选择用静态初始化替换动态初始化)在同一个翻译单元中定义的顺序。我认为标准在这一点上是非常清楚的,至少从C17开始(参见[basic.start.dynamic]/3.1.1)。
Sequenced-before”也暗示初始化发生在同一个线程中。(这只是因为你所有的变量都有 ordered dynamic initialization 并且定义在同一个翻译单元中!如果不是这样,3.1.1将不适用,并且你不能保证初始化将发生在哪个线程上。)
因此,ty的初始化将发生在同一线程上,并且yt之后。但是,由t的初始化启动的线程将与此线程并行运行。
因此,y的初始化和cout << "y = " << y << '\n';y的访问在线程内部没有任何同步,这两个过程在两个线程中并行发生,因此这是一个数据竞争,导致未定义的行为。
出于同样的原因,您也不能保证y的初始化不会在threadFuncx读取时写入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标准中没有关于线程的内容,一切都取决于实现如何处理线程。所以这可能是当时的常见行为。
但正如您在上面看到的,无论如何,仍然很容易导致数据竞争,本书从引用部分得出的指导方针仍然适用,应该遵循。

相关问题