比 main 更长寿的单身人士
Posted
技术标签:
【中文标题】比 main 更长寿的单身人士【英文标题】:Singletons that outlive main 【发布时间】:2021-01-26 19:05:29 【问题描述】:“神奇的静态”单例方法通常非常有效:
T& instance()
static T inst;
return inst;
以线程安全的方式,它在第一次调用时创建T
,并且该对象一直存在到程序关闭。但是,如果这是一个日志记录对象,并且如果有后台线程比 main 更持久,这可能会崩溃。我看到有人提到了这个问题,但没有提到解决方案:
这是一种可行的方法吗?:
/*static*/ T& T::instance()
static auto inst = std::make_shared<T>();
static thread_local auto threadInst = inst; // Hope this call gets hit before `inst` goes out of scope.
return *threadInst;
据我了解,只要每个线程在 main 退出之前调用 T::instance()
,该线程就会正确设置 threadInst
,只要调用线程正在运行,它就会使对象保持活动状态。
问题:
-
正确吗?只要
T::instance()
在main()
开始之后和main()
结束之前被每个线程首先调用,以上是否会避免UB?
有没有办法放松这个限制?我不知道有。特别是,我想不出一种方法来销毁inst
与其他线程进行通信。
【问题讨论】:
"...如果后台线程比主线程更持久,.." 这就是问题所在。现在运行时(和操作系统卸载共享库)正在关闭并删除对仍在运行的任何线程的支持。 “飞行员刚跳下飞机,但我相信我们会没事的。”当你的主线程退出时会发生什么。 为什么不能强制main()
成为最后一个退出的线程?
可以直接泄漏对象而不是破坏它吗?
如果你依赖于共享指针的引用计数,那么就让调用者自己处理它的副本。如果它实际上只是在所有线程之间共享的同一个实例,为什么还要为这个线程本地单例而烦恼呢?
【参考方案1】:
其他问题没有提供解决方案的原因是在大多数情况下它不可能有用。允许线程超过 main 很难编写和维护。一些规则是:
[basic.start.term/4] 如果函数包含已销毁的静态或线程存储时长的块变量,并且在具有静态或线程存储时长的对象的销毁期间调用该函数,则程序有如果控制流通过先前销毁的块变量的定义,则未定义的行为。
因此,如果不能保证在静态存储销毁之前执行,则不能使用函数局部静态。
[basic.start.term/6] 如果在信号处理程序中不允许使用标准库对象或函数,并且在完成具有静态存储持续时间的对象销毁和执行
std::atexit
注册之前不会发生函数,程序具有未定义的行为。
所以,如果你不能保证在静态存储销毁之前执行,你也无法使用大部分标准库。
[support.signal/3.1] [注 1:这隐含地排除了依赖于库提供的内存分配器的 new 和 delete 表达式的使用。 ——尾注]
所以你不能直接或间接使用标准的 new 或 delete。
回到你的例子:
/*static*/ T& T::instance()
static auto inst = std::make_shared<T>();
static thread_local auto threadInst = inst; // Hope this call gets hit before `inst` goes out of scope.
return *threadInst;
除非您与线程协调,以便调用它并且在detach
之前构造thread_local
,否则您只是将一场比赛替换为另一场比赛。要以这种方式稳健地执行某些操作,您必须在分离之前等待条件变量让线程通知您 thread_local 已初始化。但是,如果这样做,通常直接将指针传递给线程会更简单。此外,shared_ptr
不允许在信号处理程序中使用,因此它不是您可以用于此目的的类型。
所以,这是我的指导方针:
T 或其使用或调用的任何东西不能保证在静态存储销毁之前发生,不能使用任何非信号安全的东西,包括大多数标准库。 将指向 T 的指针或引用传递给线程。它不能通过函数局部静态或全局静态变量访问。它必须在相关线程中保持为 thread_local 或自动。 泄漏它。您不能销毁它,因为在这种情况下不允许删除。【讨论】:
以上是关于比 main 更长寿的单身人士的主要内容,如果未能解决你的问题,请参考以下文章