【问题标题】:Portable C++ Singleton - When is the destructor calledPortable C++ Singleton - 何时调用析构函数
【发布时间】:2018-09-01 12:53:59
【问题描述】:

当必须使用 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;
}

A. Williams 在他的《C++ Concurrency in Action》一书中写道,自 C++11 以来,“初始化被定义为恰好发生在一个线程上”,因此“可以用作 std::call_once 的替代方案”当需要单个全局实例时。当像上面定义的那样调用单例的析构函数时,我会感到很痛苦。

标准 (ISO/IEC 14882:2011) 定义为 §3.6.3 e 的一部分。 g.

已初始化对象的析构函数(即,其 生命周期已开始)具有静态存储持续时间的称为 从 main 返回的结果和调用 std::exit 的结果。

调用在 cstdlib 中声明的函数 std::abort() 会终止 程序不执行任何析构函数并且不调用 传递给 std::atexit() 或 std::at_quick_exit() 的函数。

那么在干净的退出(从 main 返回)首先会发生什么?是否所有线程都在“调用具有静态存储持续时间的初始化对象”的析构函数之前或之后停止?

我知道使用由共享库提供的单例是一个坏主意(它可能在其他可能使用它的部分之前被卸载)。 当 Singleton::getInstance() 被调用时会发生什么。 G。来自其他(分离的)线程?这会导致未定义的行为还是所有线程 (分离与否)在调用静态变量的析构函数之前被终止/加入?

(明确一点:我认为单例是一种反模式,但是当我必须使用它时,我想知道会发生什么样的坏事。)

【问题讨论】:

  • 我不明白这个问题,对不起。您引用了告诉您何时调用析构函数的文本。如果您想具体了解分离线程,那么您可能会对this answer 感兴趣。
  • Quoting: "静态 存储时长。对象的存储在程序开始时分配,在程序结束时释放。对象的实例只存在一个。在命名空间范围内声明的所有对象(包括全局命名空间)都具有此存储持续时间,以及使用 staticextern 声明的对象。”也看看thread_local
  • 我相信静态破坏的发生顺序与构造相反。这应该意味着单例是在 任何绑定到它的引用之前创建的。我怀疑只有当有人拿到它的地址并使用它而不是引用时才会有问题。但我不是 100% 肯定是这样......
  • 我不认为这完全是重复的,但答案可能埋在这里stackoverflow.com/questions/1008019/c-singleton-design-pattern
  • 我认为您不能在 Singleton 类之外调用 Singleton::getInstance 方法,因为它不是静态的,并且您无法创建 Singleton 的实例,因为您将构造函数声明为私有。您应该将Singleton::getInstance 设为静态。

标签: c++ multithreading c++11


【解决方案1】:

那么在干净的exit(从main 返回)首先会发生什么?是否所有线程都在“调用具有静态存储持续时间的初始化对象”的析构函数之前或之后停止?

std::exit 不需要停止任何线程,exit_Exit 也不需要。部分原因是突然终止另一个线程可能会在错误的时刻终止它并导致其他线程死锁。

当 C++ 或 C 运行时终止并通过调用 exit_group(在 Linux 上)将控制流传递回操作系统时,线程将终止:

这个系统调用等价于_exit(2),除了它不仅终止调用线程,而且终止调用进程线程组中的所有线程。此系统调用不会返回。

这意味着全局对象的析构函数与进程中的其他现有线程并行运行。在调用std::exit 或从main 返回之前,您必须以协作方式显式终止所有其他线程。

【讨论】:

  • 所以你可以说:单例(例如上面的那个)可以安全地用作多线程系统的一部分(在 C++11 中使用“便携式”std::thread 开发)当所有线程在从 main 返回之前被“加入”。使用分离的线程可能会导致意外行为。 (?)
  • @Sonic78 我宁愿使用全局对象,而任何其他名为std::exit 的线程都会导致未定义的行为。您可以让其他线程运行,只要它们不访问正在被销毁的对象。
【解决方案2】:

全局静态和单例函数静态析构函数在最后一个线程退出用户代码时以相反的顺序调用,因此不会出现多线程问题。主线程有可能退出并让其他线程继续运行。只有当所有这些线程都死掉时,程序才会真正关闭。

单例很好地避免了不能保证构造静态变量的顺序的问题,如果一个静态变量在构造过程中依赖于另一个静态变量的内容,那么行为是未定义的。使用单例,您可以根据需要有效地创建静态数据。

您必须注意的一个问题是关闭期间的凤凰单例。单例静态对象以与完成构造相反的顺序被销毁,但另一个静态/单例(A)对象是较早构造的,并且在构造期间没有调用单例(B)可能在单例(B)被销毁后,在其自身(A)销毁期间调用单例(B)。

没有实际的机制来解决这个问题,所以 std::string 使用引用计数并且不依赖于单例析构函数。

根据特定的单例,您可能能够将静态数据保持在可识别的凤凰状态,因此它可以自行复活或禁用自身。

例如,可以包装一个全局互斥体,使其在被凤凰化后实际上不会锁定。只有一个线程,所以谁在乎?如果 phoenixed,调试记录器可以在附加模式下临时重新打开日志文件,可能会发出一条消息说它被调用太晚了。请注意,这些后期创建的对象永远不会被自动销毁。

【讨论】:

  • 第一段不正确。全局对象的析构函数由std::exit调用,它可以被任何线程调用,但通常是从main返回的线程。
  • 我目前没有时间找到最近有人调试的反例,发现eclipse提前终止了调试运行。我不知道他们是如何退出的,但我知道这是可能的。
  • 最近有人声称如果你有工作线程,main 不会导致死亡:他们可能是错的:stackoverflow.com/questions/49457943/…
猜你喜欢
  • 1970-01-01
  • 2014-05-03
  • 1970-01-01
  • 2015-08-22
  • 1970-01-01
  • 2021-07-19
  • 2020-11-01
  • 2017-04-28
  • 1970-01-01
相关资源
最近更新 更多