"魔术静态"单例方法通常非常有效:
T& instance() {
static T inst;
return inst;
}
以线程安全的方式,它在第一次调用时创建T
,并且该对象一直存在到程序关闭。但是,如果这是一个日志记录对象,并且存在比 main 更持久的后台线程,则可能会崩溃。我看到一些提到了这个问题,但没有提到解决方案:
- 便携式C++单例 - 何时调用析构函数
- https://stackoverflow.com/a/1011446/874660
这是一种可行的方法吗?
/*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;
}
据我了解,只要每个线程在主线程退出之前调用T::instance()
,该线程就会正确设置threadInst
只要调用线程正在运行,该线程就会保持对象活动状态。
问题:
- 这是对的吗?只要
T::instance()
在main()
开始之后和main()
完成之前由每个线程首先调用,上述操作是否会避免 UB? - 有没有办法放松这种限制?我不知道有。特别是,我想不出一种破坏
inst
的方法与其他线程进行通信。
其他问题没有提供解决方案的原因是在大多数情况下不可能做有用的事情。允许线程通过 main 运行是很难编写和维护的。一些规则是:
[basic.start.term/4] 如果一个函数包含一个已被销毁的静态或线程存储持续时间的块变量,并且在销毁具有静态或线程存储持续时间的对象期间调用该函数,则如果控制流通过先前销毁的块变量的定义,则程序具有未定义的行为。
因此,如果不能保证在静态存储销毁之前执行,则不能使用函数本地静态。
[basic.start.term/6] 如果在信号处理程序中使用不允许使用标准库对象或函数,并且在完成具有静态存储持续时间的对象和执行
std::atexit
注册函数之前未发生,则程序具有未定义的行为。
因此,如果您不能保证在静态存储销毁之前执行,那么您也不能使用大多数标准库。
[support.signal/3.1] [注 1:这隐式排除了依赖库提供的内存分配器的新建和删除表达式的使用。
因此,您不能直接或间接使用标准的新建或删除。
回到您的示例:
/*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或自动。
- 泄露它。您无法销毁它,因为在此上下文中不允许删除。