65.9K
CodeProject 正在变化。 阅读更多。
Home

健壮的 C++:单例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (2投票s)

2020年11月22日

GPL3

7分钟阅读

viewsIcon

12013

downloadIcon

139

关于这个主题的又一篇文章?!

引言

单例模式无需过多介绍。仅在 CodeProject 上搜索“单例模式”,就会找到 50 多篇相关文章。

那么,为什么还要写另一篇文章呢?嗯,各种问题层出不穷,我想就这些问题发表一下我的看法。另外,关于 Robust Services Core (RSC) 如何实现单例模式,也存在一些微妙之处,我想在一篇文章中记录下来。我会尽量保持简洁。

对单例模式的异议

关于单例模式的 维基百科条目 链接了多篇将其视为邪恶的文章,这些文章提出了一些您在决定是否使用单例模式时应考虑的观点。

  • 单例模式就像一个全局变量.

    如果单例实例指针的访问被封装起来,并与单例数据一起,那有什么问题呢?

  • 一个类不应该关心自己是否是单例。这违反了单一职责原则,所以应该由工厂来创建单例。

    如果类在创建后不再关心,那么工厂只是样板代码,只是为了满足某种教条。几乎所有的规则都有例外,允许它们被打破。

  • 单例模式会在类之间产生紧耦合。客户端知道它是一个单例,因此您无法轻松地将其替换为多态对象,例如。

    如果只需要一个类的实例,这个论点就没有意义了。

    即使您知道最终需要支持多个实例,在某些时候使用单例模式也可能使事情变得更容易。这本身并没有错——前提是您有一个计划来演进您的软件。优秀系统是自然成长的;试图一开始就构建好一切通常会导致失败。

  • 单例模式的状态会一直存在,这会使测试变得困难.

    提供一种方法来重置或重新创建单例。

  • 当没有人使用它时,单例模式会浪费内存或资源.

    使用引用计数来销毁单例或释放其资源。如果这会造成不必要的开销,请保留它或其资源。除非您有很多低使用率的单例,否则直接将它们保留在内存中会更简单。

  • 有些语言不支持单例模式.

    这说明了那些语言的问题,而与单例模式的有效性无关。

  • 单例模式的子类化几乎是不可能的。

    那么它就不应该是单例模式。1

  • 在多线程环境中,您可能会得到多个单例实例。

    各种文章讨论了访问单例时需要锁定。这增加了大量开销,他们试图通过仅在单例尚不存在时才获取锁来最小化。但即使这样也很危险,因为仅仅访问单例的实例指针就会带来竞态条件。也许可以使用原子变量来解决这个问题,但这并不像您想象的那么容易……。

    已经有太多人为的复杂性了!单例模式只会有一个实例,所以系统初始化*期间创建它,那时应该只有一个线程在运行。如果确实需要销毁和重新创建单例,请将此责任分配给一个*特定*的线程。

所有这些都为我们使用单例模式提供了一些指导原则

  • 确保该类的一个实例始终足够。如果不是,请制定计划来演进您的软件以支持多个实例。
  • 在系统初始化期间创建单例。
  • 考虑提供一个函数,该函数将单例重置为初始状态,以简化测试。
  • 如果销毁和重新创建单例是必需的,请指定一个特定线程负责此操作。

现在我们可以看看如何实现单例模式了。

单例模板

模板的一个好处是它们避免了代码重复的需要。这使得在实现增强或修复错误时,将更改限制在一个地方。单例模式的管理就属于这一类。

首先,是单例模板的介绍性注释

//  Class template for singletons. A singleton for MyClass is created and/or
//  accessed by
//    auto c = Singleton<MyClass>::Instance();
//  This has the side effect of creating the singleton if it doesn't yet exist.
//
//  MyClass must define its constructor or destructor as private. That way, it
//  can only be created via its singleton template.  It must make this template
//  a friend class to enable access to the private constructor and destructor:
//
//    class MyClass : public Base  // actually a *subclass* of Base: see below
//    {
//       friend class Singleton<MyClass>;
//    public:
//       // interface for clients
//    private:
//       MyClass();   // cannot have any arguments
//       ~MyClass();
//    };
//
//  The type of memory that a singleton wishes to use determines it ultimate
//  base class:
//    o MemTemporary:  Temporary
//    o MemDynamic:    Dynamic
//    o MemPersistent: Persistent
//    o MemProtected:  Protected
//    o MemPermanent:  Permanent
//    o MemImmutable:  Immutable
//
//  Singletons should be created during system initialization and restarts.
//
template<class T> class Singleton
{
   // details below
};

RSC 支持重启,这是部分重新初始化系统的一种方式。为此,它提供了上述注释中提到的内存类型。每种内存类型都通过它能够存活的重启类型以及系统运行时是否是写保护来区分。现在我们将看到这在管理单例模式时引入的微妙之处。

这是创建或访问单例的函数

   //  Creates the singleton if necessary and returns a pointer to it.
   //  An exception occurs if allocation fails, since most singletons
   //  are created during system initialization.
   //
   static T* Instance()
   {
      //  The TraceBuffer singleton is created during initialization.
      //  If initialization is being traced when this code is entered
      //  for that purpose, invoking Debug::ft will create TraceBuffer,
      //  so it will have magically appeared when the original call to
      //  this function resumes execution.  We must therefore recheck
      //  for the singleton.
      //
      if(Instance_ != nullptr) return Instance_;
      Debug::ft(Singleton_Instance());
      if(Instance_ != nullptr) return Instance_;
      Instance_ = new T;
      auto reg = Singletons::Instance();
      auto type = Instance_->MemType();
      reg->BindInstance((const Base**) &Instance_, type);
      return Instance_;
   }

这里有几点需要注意

  • 此代码不是线程安全的。RSC 主要使用协同调度,因此很少需要以细粒度级别保护关键区域。如果您在 RSC 之外使用此模板,如果未遵循在初始化期间或从特定线程创建单例的建议,则需要考虑线程安全。
  • RSC 提供了一个函数跟踪工具,该工具使用一个单例跟踪缓冲区。如果启用了该工具,调用它(通过 Debug::ft)本身就会创建该单例,因此代码必须检查是否发生了这种情况。
  • 重启通过释放提供内存的堆来释放内存。因此,位于该堆上的任何单例都会消失,因此其实例指针必须置空。模板将每个单例添加到全局 Singletons 注册表中,而不是强制每个单例处理此问题。注册表的主要职责是置空将在重启时销毁的每个单例的实例指针。

前面,我们提到一些单例可能希望支持删除

   //  Deletes the singleton if it exists. In some cases, this may be
   //  invoked because the singleton is corrupt, with the intention of
   //  recreating it. This will fail, however, if the call to delete
   //  traps and our static pointer is not cleared. Even worse, this
   //  would leave a partially destructed object as the singleton. It
   //  is therefore necessary to nullify the static pointer *before*
   //  calling delete, so that a new singleton can be created even if
   //  a trap occurs during deletion.
   //
   static void Destroy()
   {
      Debug::ft(Singleton_Destroy());
      if(Instance_ == nullptr) return;
      auto singleton = Instance_;
      auto reg = Singletons::Instance();
      reg->UnbindInstance((const Base**) &Instance_);
      Instance_ = nullptr;
      delete singleton;
   }

再次,有几点需要注意

  • 单例必须从 Singletons 注册表中移除。
  • 在重启期间,不会调用 Destroy 来删除单例。重启只是释放堆,不调用堆上对象的析构函数。这使得重启比其他方式快得多。如果一个对象拥有在重启期间需要释放的资源,它必须提供一个 Shutdown 函数来释放它们。

接下来,一个非常实用的函数,在解决初始化顺序问题时非常有用

   //  Returns a pointer to the current singleton instance but does not
   //  create it.  This allows the premature creation of a singleton to
   //  be avoided during system initialization and restarts.
   //
   static T* Extant() { return Instance_; }

现在来看模板的 private 实现细节

   //  Creates the singleton.
   //
   Singleton() { Instance(); }

   //  Deletes the singleton.
   //
   ~Singleton() { Destroy(); }

   //  Declaring an fn_name at file scope in a template header causes an
   //  avalanche of link errors for multiply defined symbols. Returning
   //  an fn_name from an inline function limits the string constant to a
   //  single occurrence, no matter how many template instances exist.
   //
   inline static fn_name
      Singleton_Instance() { return "Singleton.Instance"; }
   inline static fn_name
      Singleton_Destroy()  { return "Singleton.Destroy"; }

   //  Pointer to the singleton instance.
   //
   static T* Instance_;

最后,单例的实例指针—一个 static 成员—需要初始化。请注意,这必须在类模板之后(在类模板外部)完成

//  Initialization of the singleton instance.
//
template<class T> T* NodeBase::Singleton<T>::Instance_ = nullptr;

虽然 Singletons 注册表本身也是一个单例,但它无法使用该模板,因为 Instance 函数会尝试将注册表添加给自己。因此,它从模板中克隆了它需要的代码。

静态单例模式

用户 megaadam 评论说,以下实现(在 Stack Overflow 上讨论过)从 C++11 开始就是线程安全的

Singleton& Singleton::Instance()
{
   static Singleton s;
   return s;
}

RSC 在几个地方使用此方法来解决初始化顺序问题。但是,它不支持 RSC 的内存类型,而模板支持。它只能在常规的、预分配的内存中创建单例,或者在修改为使用 new 后在默认堆上创建。不过,这是一个值得了解的有用技术。

用法

RSC 在各种情况下使用单例模式。

注册表。RSC 有许多注册表,每个注册表都跟踪所有派生自某个公共基类的对象。每个注册表都是一个单例,它使用一个标识符来访问其注册对象,该标识符区分了各种多态对象。一个模板也实现了注册表的大部分行为。

享元模式。许多注册表由享元对象填充。拥有多个享元实例非常浪费,因此每个都是单例。例如,RSC 的状态机框架定义了 ServiceStateEventHandler 类,它们的所有叶类都是享元对象,并放置在注册表中。有一个全局服务注册表,每个服务都有其状态和事件处理器的注册表。这支持一种表驱动的方法,其中服务标识符、状态标识符和事件标识符组合起来查找并调用正确的事件处理器。

内存。每个堆和对象池都由一个单例实现。

线程。RSC 有许多单例线程。将来,其中一些将不再是单例,但目前它们仍然是。

  • RootThread 包装了为 main 创建的线程。
  • InitThread 初始化系统并在系统运行时调度线程。
  • CoutThread 封装了所有写入 cout 的操作。
  • CinThread 封装了所有从 cin 读取的操作。
  • LogThread 将日志缓冲到控制台和日志文件中。
  • FileThread 封装了由多个线程写入的文件。
  • CliThread 解析并执行通过 CLI 输入的命令。
  • StatisticsThread 生成周期性统计报告。
  • ObjectPoolAudit 将泄漏的内存块返回到其对象池。
  • TimerThread 为状态机实现轻量级计时器。

注释

1 写文章的一个好处是它会让你重新审视旧代码。我的单例模板的注释提到单例的构造函数和析构函数应该是 private—如果必须支持子类,则为 protected。这个粗心的注释现在已经被修正了。

历史

  • 2020年11月25日:更新了对线程安全和静态单例模式的提及
  • 2020年11月22日:初始版本
© . All rights reserved.