分享

C ++:单例

 一只小佳 2020-11-23

介绍

单身人士几乎不需要介绍。一个搜索CodeProject上单独变成了关于他们的50余篇。

那为什么还要写另一篇文章呢?好吧,各种各样的问题不断涌现,我想讨论一下。关于稳健服务核心(RSC)实现单例的方式也存在一些缺陷,我想在一篇文章中进行记录。我会尽力使它变得麻木。

反对单身人士

在维基百科条目上单身链接到认为他们的邪恶,和这些条款作出决定是否使用一个单身的时候,你应该考虑的一些点多篇文章。

单例就像一个全局变量。

如果将对单例实例指针的访问与单例数据一起封装,则问题何在?

一个类不必关心它是否是单例:这违反了单例责任原则,因此工厂应改为创建单例。

如果班级创建后不关心,那么工厂就是样板,放在那儿以满足宗教命令。几乎每个规则都具有允许其被打破的例外。

单例会在类之间建立紧密的耦合:客户知道这是一个单例,因此,例如,您无法轻松地将其替换为多态的东西。

如果只需要一个类的一个实例,则此参数没有任何意义。

现在,即使您知道最终将需要支持多个实例,有时也可能使用单例使事情变得容易。这没有什么不对的,前提是您已制定了如何开发该软件的计划。好的系统是有机增长的;从一开始就正确构建一切通常是通往失败的道路。

单例状态持续存在,这可能会使测试变得困难。

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

当没有人使用单例时,它会浪费内存或资源。

使用引用计数破坏单例或释放其资源。如果这导致不必要的开销,请保留它和/或其资源。

有些语言不容易支持单例。

这说明了这些语言,但没有说明单例的有效性。

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

那么它不应该是单例。1个

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

各种文章讨论了访问单例时锁定的必要性。这增加了很多开销,他们试图通过仅在单例不存在的情况下获取锁来尽量减少开销。但这甚至是危险的,因为简单地访问单例的实例指针会打开争用条件的大门。也许可以通过使用原子变量来解决,但这并不像您想的那么容易……。

足够的人为复杂性!单例只会有一个实例,因此在系统初始化期间应只运行一个线程来创建它。如果有时候确实需要销毁并重新创建单例,则将此职责分配给特定线程。

所有这些都为我们提供了一些使用单例的准则:

确保仅使用一个类实例就足够了。如果不是这样,请制定一个计划,以发展您的软件以支持多个实例。

在系统初始化期间创建单例。

考虑提供一个将单例返回其初始状态的函数,以简化测试。

如果需要销毁并重新创建单例,请为此创建一个专用线程。

现在我们来看看如何实现单例。

单例模板

模板的优点之一是它们避免了重复代码的需要。实施增强或错误修复时,这会将更改限制在一个地方。单例的管理属于这一类。

首先,对单例模板进行介绍性评论:

RSC支持重新启动,这是部分重新初始化系统的一种方式。为此,RSC提供了以上注释中提到的内存类型。每种内存类型的特征在于哪些重新启动类型可以释放它,以及在系统运行时是否具有写保护。现在,我们将看到在管理单例时引入的皱纹。

这是创建或访问单例的函数(https://www./):

// 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(){
// When initialization is being traced, tracing this function
// will create singletons used by TraceBuffer, so 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_;}12345678910111213141516171819

这里有两件事要注意:

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;}12345678910111213141516171819

同样,有两件事:

必须从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_; }12345

现在查看模板的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_;123456789101112131415161718192021

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

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

尽管Singletons注册表也是单例,但它不能使用模板,因为该Instance函数会尝试将注册表添加到自身。因此,它将从模板中克隆所需的代码。

用法

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

登记处。RSC有许多注册中心,每个注册中心都跟踪从一个公共基类派生的所有对象。每个注册中心都是一个单例,它使用区分各种多态性的标识符来提供对其注册者的访问。一个模板也实现了很多注册表的行为。

举重。许多注册机构都有举重者。拥有多个飞重实例非常浪费,因此每个实例都是一个单例。例如,RSC的状态机框架定义了类Service,State和EventHandler,它们的所有叶类都是放置在注册表中的flyweight。有一个用于服务的全局注册表,每个服务都有用于其状态和事件处理程序的注册表。这支持表驱动的方法,其中服务标识符,状态标识符和事件标识符结合起来以查找并调用正确的事件处理程序。

记忆力。每个堆和对象池均由单个实例实现。

线程。RSC有许多单例线程。有一天,其中一些将不再是单例,但现在它们仍然是:

RootThread包装为创建的线程main。

InitThread 初始化系统并在使用后调度线程。

CoutThread前端全部写入cout。

CinThread前端从读取所有内容cin。

LogThread 后台打印日志到控制台和一个日志文件。

FileThread 前端文件由多个线程写入。

CliThread 解析并执行通过CLI输入的命令。

StatisticsThread 生成定期统计报告。

ObjectPoolAudit 将泄漏的内存块返回到其对象池。

TimerThread 为状态机实现轻量级计时器。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多