线程安全的单例模板和管理 - 无需 MFC!






4.71/5 (13投票s)
2002年12月8日
9分钟阅读

129179

916
线程安全单例和单例管理的代码和说明。
目录
引言
模式是针对常见开发问题而设计的解决方案。正确应用模式(而不仅仅是为了使用模式本身!)可以创建可扩展且灵活的应用程序。
简而言之,单例就像是面向对象(OO)友好的全局变量,可以被类共享——好处是无论在哪里使用它,都保证它是该类的唯一实例。与创建时间随机(由编译器通过您无法控制的机制决定)的静态全局变量不同,您还可以决定单例的创建顺序——如果一个单例依赖于另一个单例先存在,这一点至关重要。
这个特定的模式有很多实现和解释——本网站上有一些有趣的例子,我在此展示我的。那么,我的单例实现有何不同之处呢?
- 它附带了一个 Observer 类,可以清理所有单例。
- 它在设计时就考虑了线程安全。
- 它有三种形式——一个简单、可修改且不使用模板的示例,一个用于任何类的单例容器,以及一个开发者可以从中继承的单例模板,只需几行代码即可使一个类成为单例。
- 它不使用 MFC(但对于那些离不开它的人(*咳嗽*
CString
),它也可以用于 MFC 应用程序)。 - 它很可能只需最少的麻烦就可以移植到其他平台。
- 这是我利用业余时间完成的,是我自己的作品,注释详尽,是一次很棒的学习经历,并且在我看来相当不错!
与其他 CodeProject 上的文章相比,我将本文评为初级/中级。代码本身可能不是完美的,还可以改进,但它应该能很好地说明单例模式,并传达了我试图实现的目标。
背景
我编程多年,于 2001 年 6 月以二等荣誉学位毕业——计算机研究学士(理学士)。模式、UML 以及其他 OO 和标准编程技术都是课程的一部分。许多人看不到模式的意义,可能是因为设置的项目似乎根本不值得麻烦。而且这一切都是文档工作的一部分,而大多数人觉得这没什么意思!
进入现实世界(对我来说,这是我交完最后一份作业一周后开始的!),我工作的公司要求具备这些 OO 和 UML 技能,但他们的项目却很少使用这些技能。许多产品的技术文档稀疏或不存在,甚至有用的代码内注释也很少。被扔进“深水区”,要求维护他们一个较大的产品,同时还要为他们想要我开发的新产品只给一张 A4 纸,这相当令人兴奋。
一年多过去了,我从同事和自己的经验中学到了很多。很少有人喜欢编写技术设计文档、测试或保持产品文档的最新——但一开始有这些东西真的、真的会有帮助!我所做的一切都会进行大量的注释,维护一个日志本,并尽量保持技术文档的最新——以防我中了彩票,然后其他不幸的人不得不接管我所做的工作!
在花费了六个月的时间为现有产品编写了大量注释,并制作了一个直接编码的原型来证明新产品的技术可行性之后,我决定“止损”,几乎从零开始重新设计我正在开发的新产品(除了可转移的功能)。我为什么要这样做?因为新产品的规格发生了足够大的变化,以至于我不得不围绕我自己的代码进行修补才能让事情正常运行,而我不想事情变成那样。
我已成功地将单例模式应用于许多不同项目的多个方面——但每次我都改进了我对该模式的解释。但最终,我开始在更复杂的应用程序中遇到“奇怪的”线程类型错误——那些虽然发生概率极低(百万分之一),但却发生在最糟糕时机的讨厌错误。每次使用复制粘贴(一种“反模式”[5])一段代码时,我也会感到 sedikit 不适——因为这意味着我在本不应该重复的地方重复自己(可能还有错误!)。
在 2002 年 11 月的最后一个星期,我利用了大量的业余时间研究轻量级且无需 MFC 的线程同步技术[1],并进一步研究了单例模式 [2,3,4]。每个文件都是从零开始的(没有复制粘贴!),并通过我自己的想法和我的参考资料中的想法构建而成——我觉得它足够独特,可以作为我自己的作品发布并与您分享。
使用代码
使用代码 - 快速版
为防止类名冲突,我将所有这些实用类都放在我的命名空间(我的首字母 PDE
)中。对于新手来说,您可以使用作用域解析运算符 ::
来访问不同命名空间中的函数和类。
要使用模板方法,只需遵循 CSingletonExample.h 中的示例。公有继承自 PDE::TSingleton< CSingletonExample >
,并可选择继承其中一个线程同步类。
#include "TSingleton.h" #include "CLockableObject.h" class CSingletonExample : public PDE::TSingleton< CSingletonExample >, public CLockableObject { //You must make the template base a friend so //it can use the protected constructor friend class TSingleton< CSingletonExample >; protected: // Constructor must be private or protected so no //other object can initiate its creation. //Must have appropriate default constructor! CSingletonExample(); public: // The Destructor must be public so that //other classes can trigger this object to properly //clean itself up ~CSingletonExample(); public: // Class workings, blah blah blah... /// and so on... }
为了节省在 PDE
命名空间中的类输入,您可以在 cpp 文件中,紧跟在 include 之后加上这段代码...
using namespace PDE;
构造函数必须是保护的或私有的。由于迄今为止我被迫使用了友元机制,所以哪一个都可以。要发生正确的清理,您必须将析构函数设为公有的。
证明通过 TSingleton
生成的单例是线程安全的示例测试存根包含在 SingletonTemplate.cpp 中。下面显示了一个简化的测试存根。
#include "CSingletonExample.h" #include "CSingletonObserver.h" //.... etc etc .... using namespace PDE; //.... etc etc .... int main(int argc, char* argv[]) { //.... Do something interesting // Get the pointer to the one and only instance. // This value may be NULL if //the unique instance has been killed. CSingletonExample* pCSingleton = CSingletonExample::Instance(); //Check we have a valid pointer... if (pCSingleton) { //Use it as you would anything else pCSingleton->SetTestInt(1); CSingletonExample::Instance()->DebugShow(); } //The Observer must be called to delete //the memory taken up by the Singleton CSingletonObserver::Instance()->RemoveAllAndDie(); }
提供的类在其构造函数和析构函数中使用 printf
来证明它们在做什么,如果您编译项目并运行 exe,您就会看到。请注意,在最后是如何请求 CSingletonObserver
类的实例,然后指示它清理所有单例以及它自己。
就是这样!单例本身的创建是线程安全的,当然,如果您创建的任何方法,您都应该自己使其线程安全——如果单例要在多线程应用程序中使用。项目中其他的类可能会给您一些关于如何做到这一点的想法。
使用代码 - 长版
如果有人对代码的其他方面提出足够多的问题,将在此处添加内容。
有趣的点
文章模板说:“在编写代码时,您是否学到了任何有趣/好玩/烦人的东西?您是否做了什么特别聪明、疯狂或古怪的事情?”嗯——我想各种都有!
有趣 & 好玩
将单例的概念进一步推向唯一性的领域——通过创建一个默认行为,即一次生命周期——一旦它被创建和销毁——就结束了——它就不能重生。仔细想想,这是有道理的——只有一个东西意味着当它消失时,它就永远消失了。这也确保了在单例观察者完成清理所有单例后,单例不会被重新创建。
我借此机会创建了一些基本的线程同步辅助类,这些类虽然在这里的示例中使用——但我目前正在试验并使用它们。
烦人
我可以理解单例的析构函数需要是公有的才能进行正确的清理,这似乎有些道理。但是,有件事更让我感到困扰。当从 TSingleton
创建单例时,您的类就成为该层级结构的一部分,如果您像我描述的那样继承它。然而,TSingleton
在 Instance
中无法实例化单例,除非继承的单例声明 TSingleton
是一个友元类。这一点我不太明白,因为 TSingleton
本质上是单例本身的一部分,所以它应该能够访问保护和私有成员。不过,我想我这样做确实在某种程度上“挑战”了语言,所以也许我很幸运 C++ 允许我做到这一步!
疯狂或古怪
模板本身继承一个普通类是相当酷的。尽管我查找了继承自模板的类,但在编写这篇文章和相关源代码之前,我并没有找到任何例子,只找到了一个简短的说明说它可以做到 [6]。但当然,在写完这些内容时,我确实开始找到了一些例子 [8]——这大概就是墨菲定律吧!
最后说明
如果这篇文章或源代码对您有所帮助,或者激发了您的一些想法,请给我发电子邮件。听到积极的反馈总是很高兴!当然,如果您认为自己改进了它,我将非常感兴趣地想知道如何改进!
如果我有时间,我将扩展那些线程同步类,并提供一个可以在此发布的示例,如果大家有足够的兴趣。也许我也会在我的个人网站上维护这些类 [7]。
希望这篇文章对您有所帮助,祝您在您所做的任何事情上都好运!
参考文献
请注意,指向网络资源的链接可能不准确,这是网络的本质。
- - Portable Thread Synchronization Using C++, Jim Frost (1995).
- - "Design Patterns - Elements of Reusable Object-Oriented Software", Erich Gamma et al (1995).
- - "Applying UML and Patterns", Craig Larman (1998).
- - "Singleton Creation the Thread-safe Way", Jonathan Ringle (1999).
- - "Anti-patterns", - <包含反模式信息的链接的网站> William Brown et al, (1998).
- - "C++ How To Program - Forth Edition", Deitel & Deitel (2003).
- - 我的主页 (Enjoysoftware) - www.enjoysoftware.co.uk, Paul Evans (2003).
- - "COM Singletons - A Dangerous Animal", Richard Blewett (2001).
延伸阅读
感谢 Jonathan de Halleux,他提供了 一个包含不同单例模板集的链接,以及其他一些模式。这是“Loxi”库的移植版本,非常值得一看,可以获得一些额外的想法。
历史
- 版本 1 - 08-DEC-02
- 提交的第一个修订版。
- 版本 1.1 - 09-Dec-02
- 添加了“延伸阅读”部分