thread_local变量和全局变量的初始化顺序是什么?

7

C.h:

#include <iostream>

class C {
public:
  explicit C(int id) { std::cout<<"Initialized "<<id<<"\n"; }
};

1.cpp:

#include "C.h"

C global(1);

2.cpp:

#include "C.h"

thread_local C thread(2);

int main() {}

我的问题是:是否保证global会在thread之前被初始化?
据我所知,C++标准在这一点上有些含糊。根据C++17 n4659草案,它说: [basic.start.static] 静态初始化 具有静态存储期的变量将作为程序启动的结果而被初始化。 具有线程存储期的变量将作为线程执行的结果而被初始化。
理所当然地,“程序启动”发生在“线程执行”之前,但由于这两个表达式只出现在标准的那个位置,因此我正在寻求实际语言专家的建议。

如果我没记错的话,标准中有保证。但是所有供应商都会在第一次使用时初始化“thread_local”变量。 - ALX23z
“程序初始化”在“线程执行”之前发生是很有道理的。我猜这取决于“程序初始化”(并执行“main()”)是否本身就是一个线程(在C ++术语中)。如果是,那么人们可能希望由“线程执行”的结果来完成的事情会在由“程序初始化”引起的事情之前发生。 - TripeHound
1
还涉及到:活跃的 CWG问题2148 - walnut
2个回答

5
我将使用C++20工作草案,因为其措辞更加清晰,尽管实际规则没有改变。首先,thread_local的行为基本上像非局部的static[basic.stc.thread]/2

[注:具有线程存储期的变量按照[basic.start.static]、[basic.start.dynamic]和[stmt.dcl]中指定的方式初始化,并且如果构造,则在线程退出时被销毁([basic.start.term])。——注解结束]

是的,这是一个注释。但是声明为thread_local的非局部对象基本上就像static,所以这是有道理的。现在,既不是global也不是thread具有常量初始化——因此两者都会进行零初始化,然后它们必须进行动态初始化。去[basic.start.dynamic]

如果变量是隐式或显式实例化的特化类型,那么具有静态存储期的非局部变量的动态初始化是无序的;如果变量是不是隐式或显式实例化的特化类型且为内联变量,则其动态初始化是部分有序的;否则,其动态初始化是有序的。

我们的变量都不是特化类型,也不是内联变量。因此,它们都是有序的

如果声明 D 出现在声明 E 之前,则声明 D 在出现时是与声明 E 按外观顺序排序的,条件如下:

  • D 出现在与 E 相同的翻译单元中,或
  • 包含 E 的翻译单元对包含 D 的翻译单元具有接口依赖性,

在这两种情况下均在 E 之前。

我们的声明相对于彼此没有按外观顺序排序

具有静态存储期的非局部变量 V 和 W 的动态初始化按以下顺序排序:

好的,子项目 1:

如果 V 和 W 具有按顺序初始化,并且 V 的定义在 W 的定义之前按外观顺序排序,或者如果 V 具有部分有序初始化,W 没有无序初始化,并且对于 W 的每个定义 E,都存在 V 的定义 D 使得 D 在 E 之前按外观顺序排序

不适用。这是一个复杂的条件,但它不适用。
否则,如果程序在初始化V或W之前启动了除主线程以外的线程,则无法确定V和W的初始化发生在哪些线程中; 如果它们在同一线程中发生,则初始化是无序的。
没有线程。
否则,V和W的初始化是不确定顺序的。
好了。全局和线程是不确定顺序的。
请注意:

非局部内联变量的静态存储期动态初始化是在main函数的第一条语句之前顺序执行还是延迟执行是由实现定义的。

并且:

线程存储期的非局部非内联变量的动态初始化是在线程的初始函数的第一条语句之前顺序执行还是延迟执行是由实现定义的。


你能详细解释一下“接口依赖”是什么意思吗?我在找到的标准中没有找到定义。 - Shachar Shemesh
@ShacharShemesh 你可以在index中查找术语,然后指向definition - Barry
谢谢。正如我所怀疑的那样:这是仅适用于C++20的东西。很遗憾,因为它本来对我正在尝试做的事情很有用。最终,我通过放置一个简单指针并在第一次需要时手动分配它来强制排序 :-( - Shachar Shemesh
@ShacharShemesh 嗯,它只有在模块的上下文中才有意义,所以自然而然 :-) - Barry
使用案例(在C++17中)是一个需要全局支持的基类,派生模板也应该被设置为全局。存在固有的依赖关系,但不是C++所认可的。 - Shachar Shemesh

0

目前没有任何保证和任何形式的保证 - 至少当前没有。

想象一下以下情况,您有另一个无关的static全局变量Z,它在初始化期间使用您的thread_local变量,或者在初始化期间甚至创建另一个线程并使用它。

现在恰好发生这种情况:static全局变量Z在静态全局变量global之前被初始化。这意味着thread_local变量必须在您的静态全局变量之前初始化。

注意:目前没有办法保证静态全局变量的初始化顺序 - 这是C ++的已知问题。因此,如果您在另一个全局变量中使用一个全局变量,可能会导致错误 - 在技术上是UB。不要认为它以任何方式影响thread_local变量,因为它们的初始化机制往往非常不同。


我认为这与依赖于在不同翻译单元中初始化的另一个全局变量没有任何区别。 - Shachar Shemesh
@ShacharShemesh 你是否已经得到了你的UB或者UBZ。thread_local变量可以在静态变量初始化内部使用——没有任何禁止这样做的规定。因此,它们可以在静态变量之前进行初始化。你的问题归结为全局静态变量初始化顺序的同样混乱。 - ALX23z
@ShacharShemesh 我认为你在这里问错了问题。与其问哪个先初始化,你应该问“在其他thread_local/static变量中使用thread_local/static是否可以?”一般来说,答案应该是“是的,thread_local/static变量应该在第一次使用之前初始化。所以除非你在初始化中有循环依赖关系,否则一切都没问题。”不幸的是,由于上述的惨败,情况并非完全如此 - 我非常怀疑这是故意失败的。我不认为还有其他关于这样的问题的报告。 - ALX23z
1
我不理解这个答案。只有最后一句话似乎与这个问题相关,而且它缺少了ISO标准的一个基本哲学。标准并不关心初始化机制,这取决于实现者。标准仅描述可观察行为,而不是如何实现该行为。 - MSalters
@ALX23z:即使我们假设逻辑是正确的,这仅告诉我们一个特定的顺序是不可能的。但是,由于你引入了另一个变量“Z”,现在有3!= 6个可能的顺序,因此还有5个其他可能的顺序,你没有涉及到。这似乎并不是一种改进,因为原始问题只有2种可能的顺序。另外,标准很可能会说,在初始化“thread_local”变量之前,所有静态全局变量都会被初始化;正如你描述的那样,使用未初始化的“thread_local”变量会变得明确不确定。 - MSalters
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接