C++单例模式

来源:互联网 发布:通关单数据查询 编辑:程序博客网 时间:2024/06/06 00:13

单例模式

  在一些情形下,保持类的实例只有一个非常重要。例如:一个表示文件系统的Class。一个操作系统一定是只有一个文件系统的,因此,我们希望表示文件系统的类实例有且仅有一个。单例模式 是设计模式中一种实现这一类需求的设计方法。

单例模式(Singleton),保证一个类仅有一个实例,并提供一个访问它的全局访问点。 [1]

  全局静态变量能够实现对象的全局访问,但这不能防止你实例化多个类实例。为了实现上述要求,我们需要加强类的设计,让类自身保证其实例仅有一个。也就是说,“这个类可以保证没有其它实例可以被创建,并且它可以提供一个访问该实例的方法。”[1]

  一般来说,单例类的结构如下:

class Singleton {public:    static Singleton* GetInstance(); //供用户获取单例的全局访问点protected:    Singleton();  //方便继承,同时保证类的用户无法直接构造该类的实例    Singleton(const Singleton&);private:    //class members};

C++中static对象的初始化

  C++规定,non-local static 对象的初始化发生在main函数执行之前。但C++没有规定多个non-local static 对象的初始化顺序,尤其是来自多个编译单元的non-local static对象,他们的初始化顺序是随机的。
  
  然而,对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。 non-local static对象的初始化发生在main函数之前的单线程启动阶段,所以无需担心线程安全问题。但是local static对象则不同,多个线程的控制流可能同时到达其初始化语句。

  在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。[2]具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。

  在文章的后半部分会看到,local static 对象在单例模式中有着广泛的应用,为了解决local static对象在多线程环境下的重复构造问题,程序员想出了很多方法。而C++11则在语言的规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成。 [2]

C++实现单例模式

  单例模式的实现分为两大类,懒汉模式和饿汉模式。懒汉模式的单例秉承着实例能晚一点构造就晚一点构造的思想,直到第一次使用单例时才构造单例;饿汉模式则恰好相反,即使实例永远不会被使用,实例的构造还是会早早的发生。

懒汉模式

  饿汉模式有以下几点要求:保证类实例是唯一的;提供全局可访问点;延迟构造,直到第一次使用该实例。
  
  以下是几种懒汉模式的单例模式实现.[3][8] 他们或者利用了local static 对象的特性,或者使用指针来判断对象是否是第一次初始化。在多线程条件下,则需要互斥锁以避免重复构造问题
  
  首先是《设计模式》[1]中给出的单例模式实现。

template<class T>T& Singleton() {    static T instance;    return instance;}

  这一实现在C++98中不是线程安全的,具体原因参考上一节关于static对象的初始化的讨论。

  为了解决上一段代码中存在线程安全问题,最简单的方法是使用互斥锁在开始初始化之前先获取锁。

std::mutex m;  //必须是一个全局变量而不是一个局部变量template<class T>T& Singleton() {    std::unique_lock lock(m);  //析构函数自动 unlock    static T instance;    return instance;}

  但是,线程安全问题只是出现在第一次初始化过程。然而,以上代码却为了一次初始化而使得每一次获取Singleton都要首先获取锁资源,Singleton的访问变成了串行。这显然不太合算。

  为了避免每一次都加锁我们可以事先判断是否已经初始化。Double Check Lock 是一种常用的实现手法。如下:

class Singleton;class Singleton {private:    Singleton();    Singleton(const Singleton &);public:    static Singleton& GetInstance() {        if(m_instance == nullptr) {            std::unique_lock lock(m);  //析构函数自动 unlock            if(m_instance == nullptr) {                m_instance = new Singleton;            }        }        return *m_instance;    }private:    static Singleton *m_instance = nullptr;    static std::mutex m;    //other class members}

  如果内存访问严格按照语句先后顺序进行,那么以上代码堪称完美解决了所有问题。但是,在某些内存模型中(虽然不常见)或者是由于编译器的优化以及运行时优化等等原因,使得m_instance虽然已经不是nullptr但是其所指对象还没有完成构造,这种情况下,另一个线程如果调用GetInstance()就有可能使用到一个不完全初始化的对象。(double check 中的互斥锁在此处是不起作用的)
  
  内存屏障 [7]可以强制要求内存完成屏障前的所有内存读写操作。C++11提供了Atomic实现内存的同步访问,即不同线程总是获取对象修改前或修改后的值,无法在对象修改期间获得该对象。参考资料3中给出了多个实现内存同步访问的示例。以下是利用atomic实现的版本:

class Singleton;class Singleton {private:    Singleton();    Singleton(const Singleton &);public:    static Singleton& GetInstance() {        if(m_instance == nullptr) {            std::unique_lock lock(m);  //析构函数自动 unlock            if(m_instance == nullptr) {                m_instance = new Singleton;            }        }        return *m_instance;    }private:    static std::atomic<Singleton *> m_instance(nullptr);    static std::mutex m;    //other class members}

  综合以上种种我们发现,迫使我们做各种繁琐工作的罪恶来源是C++98中没有规定local static 对象在多线程条件下的初始化行为。然而在上一节的讨论中我们知道C++11给出了规定。[2]。因此,在C++11标准下一个线程安全的单例模式可以这样实现。这种方法也被称为Meyers’ Singleton

class Singleton;class Singleton {private:    Singleton();    Singleton(const Singleton &);public:    static Singleton& GetInstance() {        static Singleton instance;  //C++11标准下local static对象初始化在多线程条件下安全           return instance;    }private:    //class members}

  是不是有一种大道至简的感觉?这和我们最开始给出的形式一模一样。但是注意,此时的语言标准变了,在C++11标准下这样写才是线程安全的。另外GCC4.3和VS2015之后的版本也开始支持该特性。[2]。

饿汉模式

  饿汉模式下同样要求单例是类的唯一实例,且需要类提供一个全局访问点。但是与懒汉模式不同,饿汉模式中单例的构造会尽可能早的进行,即使目前不会用到,甚至以后永远也不会用到。
  
  以下是几种饿汉模式的单例模式实现.[4] 从上一节的讨论中我们可以知道 non-local static对象符合饿汉模式对于单例构造应该尽早进行的要求。然而,一旦使用了non-local static对象我们就不得不小心多个non-local static 对象的初始化问题。[6]

class Singleton {private:    Singleton();    Singleton(const Singleton &);    struct InstanceCreator {        InstanceCreator() {            Singleton::m_instance = new Singleton;        }        ~InstanceCreator() {            delete Singleton::m_instance;            Singleton::m_instance = nullptr;        }    };public:    static Singleton& GetInstance() {        return *m_instance;    }private:    friend class InstanceCreator;    static Singleton *m_instance;    static InstanceCreator m_creator;};

  如果没有其他non-local static 对象使用这个单例,那么上述代码是可以正常使用的。
  
  然而,在有其他non-local static 对象使用该单例时,上述代码就会出现问题。由于non-local static对象初始顺序的不确定性,我们无法让具有依赖关系的两个non-local static对象按照依赖关系的固定顺序初始化。

  事实上,在绝大多数条件下,类的设计阶段我们无法得知会不会有一个non-local static 对象依赖于这个单例,即使可以确定目前没有其他non-local static对象依赖于这个单例,那也无法确定在以后也不会有。所以我们需要设计一个方案来应对可能出现的两种情况。以下代码可以满足这种需求

class Singleton {private:    Singleton();    Singleton(const Singleton &);    struct InstanceCreator {  //真正负责单例的构造和析构        InstanceCreator() {            Singleton::m_instance = new Singleton;        }        ~InstanceCreator() {            delete Singleton::m_instance;            Singleton::m_instance = nullptr;        }    };    struct InstanceConfirm {  //没有non-loacl static对象依赖时,保证在main函数前初始化        InstanceConfirm() {            Singleton::GetInstance();        }    }public:    static Singleton& GetInstance() {  //全局访问点        static InstanceCreator creator;  //有non-local static对象依赖时确保初始化顺序。        return *m_instance;    }private:    friend class InstanceCreator;    friend class InstanceConfirm;    static Singleton *m_instance;    static InstanceConfirm m_confirm;  //最后保证main函数之前完成初始化};

  以上代码在C++98标准下也可以正常工作,唯一的要求是在main函数之前进程是单线程的。

C++单例模式的实现总结

  在C++单例模式的多种实现中,Meyers’ Singleton是绝对的第一选择!但要注意的是,其多线程安全性在C++11的标准下才能得以保证(或者编译器特性支持)[2]。。

  如果由于各种原因(包括为兼容旧代码等等)Meyers’ Singleton无法在多线程条件下工作。那么,个人认为也没有必要为了强行实现懒汉模式而搞一大堆的同步代码[3][7][8] ,直接实现一个饿汉模式的单例即可[4]。这一过程应该注意多个non-local static 对象的初始化次序问题[6]。

【参考资料】
[1] Erich G, H Richard等著,李英军, 马晓星等译. 设计模式:可复用面向对象软件的基础. 北京:机械工业出版社, 2000.9, 84-89
[2] Stack Overflow: Is Meyers implementation of Singleton pattern thread safe?. Groo的回答.
[3] .KlayGE游戏引擎:C++中线程安全并且高效的Singleton
[4] 知乎: 求教一个百度的面试题,关于多线程单例模式的?. 知乎用户的回答
[5] 程杰 著. 大话设计模式. 北京:清华大学出版社, 2007.12, 214-219
[6] Scott Meyers 著, 侯捷 译. Effective C++(第三版)中文版,北京:电子工业出版社, 2011.1, 26-32
[7] 百度百科:内存屏障
[8] 老司机. 博客园:C++程序员们,快来写最简洁的单例模式吧

2 0