【问题标题】:initialization order of thread_local vs. global variablesthread_local 与全局变量的初始化顺序
【发布时间】:2020-03-23 12:14:59
【问题描述】:

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] 静态初始化

具有静态存储持续时间的变量被初始化为 程序启动的结果。带线程存储的变量 持续时间被初始化为线程执行的结果。

按理说“程序启动”发生在“线程执行”之前,但由于这两个表达式都只出现在标准中的那个地方,我正在寻求实际语言律师的建议。

【问题讨论】:

  • IIRC 有标准的保证。但所有供应商在首次使用时都会初始化 thread_local 变量。
  • "按理说“程序启动”发生在“线程执行”之前”我想这取决于“程序启动是什么"(并继续执行main())本身是否是线程(在 C++ 术语中)。如果它,那么人们可能期望完成的事情作为线程执行的结果发生在之前事情完成作为程序的结果启动.
  • 也相关:活跃CWG issue 2148

标签: c++ c++17 language-lawyer undefined-behavior


【解决方案1】:

我将使用 C++20 工作草案,因为那里的措辞更简洁一些,尽管真正的规则都没有改变。

首先,就非本地而言,thread_local 的行为基本上类似于 static[basic.stc.thread]/2

[ 注意:具有线程存储持续时间的变量按照 [basic.start.static]、[basic.start.dynamic] 和 [stmt.dcl] 中指定的方式进行初始化,如果已构造,则在线程退出时销毁 ( [basic.start.term])。 ——尾注]

是的,这是一张便条。但是声明为thread_local 的非本地对象基本上是static,所以这是有道理的。

现在,globalthread 都没有常量初始化 - 所以两者都是零初始化,然后它们必须进行动态初始化。致[basic.start.dynamic]

如果变量是隐式或显式实例化的特化,则具有静态存储持续时间的非局部变量的动态初始化是无序的,如果变量是不是隐式或显式实例化的特化的内联变量,则它是部分排序的,并且否则订购。

我们的变量都不是特化的,也不是内联的。所以两者都是有序

声明 D 在声明 E 之前 按外观排序 if

  • 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的初始化是不确定的。

我们去吧。 globalthread 的序列不确定。


还要注意:

具有静态存储持续时间的非局部内联变量的动态初始化是在main的第一条语句之前排序还是延迟,由实现定义。

和:

具有线程存储时长的非局部非内联变量的动态初始化是在线程的初始函数的第一条语句之前排序还是延迟,由实现定义。

【讨论】:

  • 你能详细解释一下“接口依赖”是什么意思吗?我没有在我找到的标准中找到定义。
  • @ShacharShemesh 您可以在index 中查找术语,然后可以将您指向definition
  • 谢谢。正如我所怀疑的那样:这是 C++20 唯一的东西。很遗憾,因为它对我正在尝试做的事情很有用。我最终通过放入一个简单的指针并在我第一次需要它时手动分配它来强制订单:-(
  • @ShacharShemesh 好吧,它只在模块的上下文中才有意义,所以很自然:-)
  • 用例(在 C++17 中)是一个需要全局支持它的基类,并且派生的模板也需要为全局设置。存在固有的依赖关系,但没有一个 C++ 能够识别。
【解决方案2】:

没有保证,也不能有任何形式的保证——至少目前是这样。

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

现在恰好这个static 全局变量Z 在你的静态全局变量global 之前被初始化。这意味着必须在静态全局变量之前初始化 thread_local 变量。

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

【讨论】:

  • 这不会立即发生。您描述的情况很可能是UB。
  • @ShacharShemesh 我不记得有任何规则禁止在 static 变量的初始化中使用 thread_local 变量。
  • 我认为这与依赖在不同翻译单元中初始化的另一个全局变量没有什么不同。
  • @ShacharShemesh 这是因为thread_local 是通过与static 变量完全不同的机制进行初始化的。大多数编译器只是在第一次使用时初始化它们。虽然从static 初始化的static 只是一团糟。
  • 我没有得到这个答案。只有最后一句话似乎与这个问题有关,它错过了 ISO 标准的基本理念。该标准不关心初始化机制,这取决于实现者。该标准仅描述了可观察到的行为,而不是应该如何实现该行为。
猜你喜欢
  • 2014-12-20
  • 1970-01-01
  • 1970-01-01
  • 2014-08-06
  • 1970-01-01
  • 1970-01-01
  • 2017-09-11
  • 1970-01-01
  • 2016-11-02
相关资源
最近更新 更多