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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (13投票s)

2002年12月8日

9分钟阅读

viewsIcon

129179

downloadIcon

916

线程安全单例和单例管理的代码和说明。

Sample Image - PDESingletonTemplateNoMFC.jpg

目录

引言

模式是针对常见开发问题而设计的解决方案。正确应用模式(而不仅仅是为了使用模式本身!)可以创建可扩展且灵活的应用程序。

简而言之,单例就像是面向对象(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 创建单例时,您的类就成为该层级结构的一部分,如果您像我描述的那样继承它。然而,TSingletonInstance 中无法实例化单例,除非继承的单例声明 TSingleton 是一个友元类。这一点我不太明白,因为 TSingleton 本质上是单例本身的一部分,所以它应该能够访问保护和私有成员。不过,我想我这样做确实在某种程度上“挑战”了语言,所以也许我很幸运 C++ 允许我做到这一步!

疯狂或古怪

模板本身继承一个普通类是相当酷的。尽管我查找了继承自模板的类,但在编写这篇文章和相关源代码之前,我并没有找到任何例子,只找到了一个简短的说明说它可以做到 [6]。但当然,在写完这些内容时,我确实开始找到了一些例子 [8]——这大概就是墨菲定律吧!

最后说明

如果这篇文章或源代码对您有所帮助,或者激发了您的一些想法,请给我发电子邮件。听到积极的反馈总是很高兴!当然,如果您认为自己改进了它,我将非常感兴趣地想知道如何改进!

如果我有时间,我将扩展那些线程同步类,并提供一个可以在此发布的示例,如果大家有足够的兴趣。也许我也会在我的个人网站上维护这些类 [7]

希望这篇文章对您有所帮助,祝您在您所做的任何事情上都好运!

参考文献

请注意,指向网络资源的链接可能不准确,这是网络的本质。

  1. - Portable Thread Synchronization Using C++, Jim Frost (1995).
  2. - "Design Patterns - Elements of Reusable Object-Oriented Software", Erich Gamma et al (1995).
  3. - "Applying UML and Patterns", Craig Larman (1998).
  4. - "Singleton Creation the Thread-safe Way", Jonathan Ringle (1999).
  5. - "Anti-patterns", - <包含反模式信息的链接的网站> William Brown et al, (1998).
  6. - "C++ How To Program - Forth Edition", Deitel & Deitel (2003).
  7. - 我的主页 (Enjoysoftware) - www.enjoysoftware.co.uk, Paul Evans (2003).
  8. - "COM Singletons - A Dangerous Animal", Richard Blewett (2001).

延伸阅读

感谢 Jonathan de Halleux,他提供了 一个包含不同单例模板集的链接,以及其他一些模式。这是“Loxi”库的移植版本,非常值得一看,可以获得一些额外的想法。

历史

  • 版本 1 - 08-DEC-02
    • 提交的第一个修订版。
  • 版本 1.1 - 09-Dec-02
    • 添加了“延伸阅读”部分
© . All rights reserved.