可移植的C++单例模式 - 析构函数何时被调用?

3

当需要使用C++11实现线程安全的单例时,我知道唯一正确的实现方法如下:

// header
class Singleton final {
public:
  static Singleton& getInstance();

private:
  Singleton() = default;
  Singleton(Singleton const&) = delete;
  void operator=(Singleton const&) = delete;
};

// implementation:
Singleton& Singleton::getInstance() {
  static Singleton instance;
  return instance;
}

在《C++ Concurrency in Action》一书中,A. Williams写道,自从C++11以来,“初始化被定义为只在一个线程上发生”,因此当需要单个全局实例时,它可以被用作std::call_once的替代方法。我想知道当像上述方式定义Singleton时,其析构函数何时被调用。
标准(ISO/IEC 14882:2011)在§3.6.3的一部分中定义了以下内容:
“已初始化对象的析构函数(即,其生命周期已开始)具有静态存储期和由于从main函数返回而被调用和由于调用std::exit而被调用。”
并且
“调用在cstdlib中声明的std::abort函数将终止程序,而不执行任何析构函数,并且不调用传递给std::atexit()或std::at_quick_exit()的函数。”
那么,在干净的退出(从main函数返回)时,哪个先发生?是否在“调用已初始化具有静态存储期的对象的析构函数”之前或之后停止所有线程?
我知道使用由共享库提供的单例模式是一个坏主意(因为可能在其他可能使用它的部分之前被卸载)。当例如从其他(分离的)线程调用Singleton::getInstance()时会发生什么?那是否可能导致未定义行为,或者所有线程(分离或非分离)是否在调用静态变量析构函数之前被终止/合并?
(为清楚起见:我认为单例模式是一种反模式,但当我必须使用它时,我想知道可能会发生什么坏事。)

抱歉,我不理解这个问题。你引用了描述析构函数何时被调用的文本。如果你想要了解有关分离线程的具体信息,那么这个答案可能会对你有所帮助。 - Lightness Races in Orbit
1
我相信静态销毁的顺序与构造相反。这意味着单例在任何绑定到它的引用之前被创建。我怀疑只有当某人取其地址并使用该地址而不是引用时才会出现问题。但我不是100%确定,所以... - Galik
1
我认为这不是完全重复,但答案可能埋在这里 https://dev59.com/Q3NA5IYBdhLWcg3wVcJx - Galik
1
我认为你不能在Singleton类外部调用Singleton::getInstance方法,因为它不是静态的,而且你不能创建Singleton的实例,因为你将构造函数声明为私有的。你应该将Singleton::getInstance设置为静态的。 - Mohit
你已经知道了一个未连接和未分离的线程将会调用 std::terminate,而加入的线程则是绝对安全的。此外,你可以避免使用分离的线程,那么你还想知道什么? - xskxzr
显示剩余6条评论
2个回答

2
“clean exit”指的是从主函数main返回时发生的情况。在调用析构函数之前或之后,所有线程是否都停止运行呢?std::exitexit_Exit没有要求停止任何线程,这部分是因为突然终止另一个线程可能会在错误的时刻终止它,并导致其他线程死锁。当C++或C运行时通过调用exit_group(在Linux上)将控制流传递回操作系统时,线程将被终止:
“该系统调用相当于_exit(2),但它不仅终止调用线程,而且终止调用进程的线程组中的所有线程。此系统调用不返回。”
这意味着全局对象的析构函数与您进程中的其他线程并行运行。在调用std::exit或从main返回之前,您必须明确地以合作方式终止所有其他线程。

因此,您可以说:单例(例如上面的那个)可以安全地用作多线程系统的一部分(使用“可移植”的std :: thread在C ++ 11中开发),当所有线程在从主函数返回之前“加入”时。使用分离的线程可能会导致意外行为。 (?) - Sonic78
@Sonic78 我宁愿在其他线程调用 std::exit 时不使用全局对象,以避免未定义的行为。只要这些线程不访问正在被销毁的对象,你可以让它们继续运行。 - Maxim Egorushkin

0
全局静态和单例函数的静态析构函数在最后一个线程退出用户代码时以相反的顺序调用,因此不存在多线程问题。主线程可能会退出并留下其他线程运行。只有当所有这些线程死亡时,程序才真正关闭。
单例模式非常适合避免静态构造顺序没有保证的问题,如果一个静态对象在构造期间依赖于另一个静态对象的内容,则行为是未定义的。使用单例模式,您的静态对象实际上是按需创建的。
您需要注意的一个问题是Phoenix单例模式在关闭期间。单例静态对象的销毁顺序与它们完成构造的顺序相反,但是在构造早期构造的另一个静态/单例(A)对象,在其自身(A)销毁之后,可能会在单例(B)已被销毁后调用单例(B)。
实际上没有机制来处理这个问题,所以std::string使用引用计数,并且不依赖于单例模式的析构函数。
根据特定的单例模式,您可以将静态数据保留在可识别的Phoenix状态中,以便它可以自我恢复或禁用自身。
例如,全局互斥锁可以被包装,以便在被“phoenixed”后实际上不会锁定。只有一个线程,谁在乎呢?如果被“phoenixed”,调试记录器可以暂时以追加模式重新打开日志文件,也许发出一条消息以表明它被调用得太晚了。请注意,这些晚创建的对象将永远不会自动销毁。

第一段是不正确的。全局对象的析构函数是由std::exit调用的,它可以被任何线程调用,但通常是从main返回的线程调用。 - Maxim Egorushkin
我目前没有时间去找最近的反例,即有人调试时发现Eclipse提前终止了调试运行。我不知道他们是如何退出的,但我知道这是可能的。 - Gem Taylor
这里有一个最近的说法,如果你有工作线程,main函数不会导致程序死亡:但他们可能是错误的:https://stackoverflow.com/questions/49457943/program-doesnt-stop-after-returning-0-from-main - Gem Taylor

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