【问题标题】:Is it legal to initialize a thread_local variable in the destructor of a global variable?在全局变量的析构函数中初始化 thread_local 变量是否合法?
【发布时间】:2016-11-02 23:12:31
【问题描述】:

这个程序:

#include <iostream>

struct Foo {
    Foo() {
        std::cout << "Foo()\n";
    }

    ~Foo() {
        std::cout << "~Foo()\n";
    }
};

struct Bar {
    Bar() {
        std::cout << "Bar()\n";
    }

    ~Bar() {
        std::cout << "~Bar()\n";
        thread_local Foo foo;
    }
};

Bar bar;

int main() {
    return 0;
}

打印

Bar()
~Bar()
Foo()

对我来说(GCC 6.1、Linux、x86-64)。 ~Foo() 永远不会被调用。这是预期的行为吗?

【问题讨论】:

  • 合法与否,你为什么要这么做?
  • @DavidHaim 我正在尝试实现libc++abi 的一部分(特别是__cxa_thread_atexit()),并且很好奇我是否应该处理这种情况。
  • 这可能只是coutfoo 之前被销毁。尝试从Foo 的析构函数中抛出异常,看看是否调用了std::terminate
  • @BaummitAugen 请参阅en.cppreference.com/w/cpp/io/ios_base/Init,了解 C++ 如何解决std::c{in,out,err} 的初始化顺序惨败
  • @BrianCain: 是的,这与当前的语言演变是一致的,其目的是考虑在 之前 静态销毁所有线程局部变量,以便线程局部析构函数可以依赖关于静态对象的存在。

标签: c++ multithreading c++11 language-lawyer thread-local-storage


【解决方案1】:

标准不包括这种情况;最严格的解读是,在具有静态存储持续时间的对象的析构函数中初始化thread_local 是合法的,但允许程序继续正常完成是非法的。

问题出现在[basic.start.term]:

1 - 具有静态存储持续时间的已初始化对象(即生命周期 ([basic.life]) 已开始的对象)的析构函数 ([class.dtor]) 被调用作为从 main 返回的结果,结果调用 std::exit ([support.start.term])。作为从该线程的初始函数返回的结果以及该线程调用 std::exit 的结果,调用了在给定线程中具有线程存储持续时间的已初始化对象的析构函数。在该线程中具有线程存储持续时间的所有已初始化对象的析构函数的完成在任何具有静态存储持续时间的对象的析构函数的启动之前进行排序。 [...]

所以bar::~Bar::foo::~Foo的完成顺序在bar::~Bar的启动之前,这是一个矛盾。

唯一的出路可能是争辩说 [basic.start.term]/1 仅适用于其生命周期在程序/线程终止点开始的对象,但 相反 @987654322 @有:

5 - 当且仅当它被构造时,具有静态或线程存储持续时间的块范围对象的析构函数将被执行。 [注意:[basic.start.term] 描述了具有静态和线程存储持续时间的块范围对象被销毁的顺序。 ——尾注]

这显然只适用于正常的线程和程序终止,通过从 main 或线程函数返回,或通过调用 std::exit

另外,[basic.stc.thread] 有:

具有线程存储持续时间的变量应在其首次使用 odr 之前进行初始化 ([basic.def.odr]),如果已构造,则应在线程退出时销毁。

这里的“shall”是给实现者的指令,而不是给用户的。

请注意,开始析构函数范围 thread_local 的生命周期没有任何问题,因为 [basic.start.term]/2 不适用(它之前没有被销毁)。这就是为什么我相信当您允许程序继续正常完成时会发生未定义的行为。

之前有人问过类似的问题,不过是关于静态与静态存储持续时间,而不是thread_local 与静态; Destruction of objects with static storage duration(和https://groups.google.com/forum/#!topic/comp.std.c++/Tunyu2IJ6w0)和Destructor of a static object constructed within the destructor of another static object。我倾向于同意 James Kanze 关于[defns.undefined] 在这里适用的后一个问题,并且行为是未定义的,因为标准没有定义它。最好的方法是让有资格的人打开缺陷报告(涵盖在statics 和thread_locals 的析构函数中初始化的statics 和thread_locals 的所有组合),希望得到一个确定的答案。

【讨论】:

    【解决方案2】:

    把你的程序写成

    #include <iostream>
    
    thread_local struct Foo {
        Foo() { std::cout << "Foo()\n"; }
        ~Foo() { std::cout << "~Foo()\n"; }
    } t;
    struct Bar {
        Bar() { std::cout << "Bar()\n"; }
        ~Bar() { std::cout << "~Bar()\n"; t; }
    } b;
    
    int main() {
        return 0;
    }
    

    如果Foo 不是thread_local,则Foo tBar b 处于相同位置,并且可以在Bar b 之前破坏Foo t

    在这种情况下,当在b.~Bar() 中引用t 时,它指的是一个已破坏的结构,IMO 应该是一个 UB(在某些系统上,破坏结构释放其内存)。

    因此,添加thread_local 仍然是未定义的行为

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-08-06
      • 2017-09-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-10-20
      • 1970-01-01
      相关资源
      最近更新 更多