单例设计模式

  ·   5 min read

一个实例和两个问题

经常有这样一些特殊的类:必须保证它们在系统中只存在一个实例,才能确保其逻辑正确性。但是一般的constructor在运行期间允许多次调用以生成多个实例,在此场景下,为了避免客户端直接使用构造函数,需要提供一种机制来保证一个类只有一个实例。一个设计好的类应该“Easy To Use And Hard To Misuse”,因此思考上述问题应该是类设计者的责任,而不是使用者的责任。

uml

正确实现单例模式须考虑两个基本问题:线程安全和顺序安全。线程安全问题通过加锁或使用同步原语基本可以得到解决,但也会面临性能变差的权衡。由于C++11以后static的data racing问题已得到妥善解决(参考静态局部变量),故目前主流的单例模式都是基于这种写法,如AOSP的libutils模块从2017年起就禁止用单锁方式实现单例:

// DO NOT USE: Please use scoped static initialization.
// For instance:
//     MyClass& getInstance() {
//         static MyClass gInstance(...);
//         return gInstance;
//     }
template <typename TYPE>
class ANDROID_API Singleton
{
public:
    static TYPE& getInstance() {
        Mutex::Autolock _l(sLock);
        TYPE* instance = sInstance;
        if (instance == nullptr) {
            instance = new TYPE();
            sInstance = instance;
        }
        return *instance;
    }

    static bool hasInstance() {
        Mutex::Autolock _l(sLock);
        return sInstance != nullptr;
    }

protected:
    ~Singleton() { }
    Singleton() { }

private:
    Singleton(const Singleton&);
    Singleton& operator = (const Singleton&);
    static Mutex sLock;
    static TYPE* sInstance;
};

构造析构顺序问题则可分为初始化顺序和销毁顺序进行讨论。C++的初始化有很多种,根据cppref - 初始化中写的:所有具有静态存储期的非局部变量的初始化会作为程序启动的一部分在main函数的执行之前进行(除非被延迟);所有具有线程局部存储期的非局部变量的初始化会作为线程启动的一部分进行,按顺序早于线程函数的执行开始。可知全局和局部静态变量的初始化发生于两个截然不同的阶段。又由于同一个翻译单元内对象是从上到下初始化的,但不同翻译单元中具有静态存储期的对象的初始化顺序不确定(详见Static Initialization Order Fiasco),因此我们写代码时不能假设某个全局变量在另一个全局变量之前初始化完成,必须保证全局变量在其构造函数中不能调用其他全局变量提供的接口,因为实际运行中那个被调用接口的对象有可能还没有来得及初始化,构造函数未执行,成员变量没有初始化,各种资源可能没来得及申请。而单例模式通常会有一个静态函数用于返回单例类的对象的引用,这样就保证了在使用这个单例对象时该对象肯定是被初始化了的。

单例模式在解决全局变量初始化问题的同时又引入了一个新的问题,由于单例解决这个问题是通过“静态局部变量在控制流首次经过它的声明时才会被初始化(除非它被零初始化或常量初始化,这可以在首次进入块前进行)”这一点来保证,但是块作用域静态变量的析构函数在初始化已成功的情况下直到程序退出时才被调用,而多个静态对象的析构顺序是不确定的。当静态对象在析构时依赖了另一个静态对象就会出现问题,其原理与构造顺序不确定类似。即由于析构顺序是随机的,多个静态变量在销毁时也不能存在相互依赖关系。完整的解决方案可以参考《Modern C++ Design》中的phenix singleton。

Who is responsible for destructing the block scoped static Singleton instance? 介绍了局部静态变量初始化机制的核心其实是C标准库提供的exit()atexit(...)函数,在程序的编译过程中,编译器会在你的块作用域静态变量周围注入一些其他代码。下面是一个返回静态局部变量的引用的静态函数的编译期伪码:

static MyClass &instance() {
    static MyClass inst;

    /*** COMPILER GENERATED ***/
    static bool b_isConstructed = false;
    if (!b_isConstructed) {
        ConstructInstance();
        b_isConstructed = true;
        // Push dctor to the list of exit functions
        atexit(~MyClass());
    }
    /*** COMPILER GENERATED ***/

    return inst;
};

Meyer’s Singleton

铺垫结束,下面介绍Meyer’s Singleton。从C++11起普遍利用使用静态局部变量来实现线程安全的单例模式。因为这种写法的发明者是Scott Meyers,因此也被称为Meyer’s Singleton。下面是C++11标准中对静态存储期(static storage duration)更为详细的描述:

The zero-initialization of all block-scope variables with static storage duration or thread storage duration is performed before any other initialization takes place. Constant initialization of a block-scope entity with static storage duration, if applicable, is performed before its block is first entered. An implementation is permitted to perform early initialization of other block-scope variables with static or thread storage duration under the same conditions that an implementation is permitted to statically initialize a variable with static or thread storage duration in namespace scope. Otherwise such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration. If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization. If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.

一种朴素实现

class Singleton {
public:
  static Singleton &getInstance() {
    static Singleton instance;
    return instance;
  }

  Singleton(const Singleton &) = delete;
  Singleton &operator=(const Singleton &) = delete;

  string method() { return "Singleton pattern"; }

private:
  Singleton() { cout << "Singleton init" << endl; }
  ~Singleton() {}
};

int main() {
  cout << "Enter" << endl;
  cout << Singleton::getInstance().method() << endl;
  cout << "Leave" << endl;
}

输出:

Enter
Singleton init
Singleton pattern
Leave

很明显,Meyer’s Singleton是java选手们所谓的lazy init,冲上来就初始化的那个写法用的是非局部静态变量:

class My {
   public:
    My() { cout << "my init" << endl; }
};

static My m;

int main() { cout << "main" << endl; }

输出:

my init
main

至于什么时候用static,推荐阅读一篇1996年的文章:Statics: Schizophrenia for C++ Programmers

基于模板的代码生成技术

Meyer’s Singleton的写法固定,因此如果系统中单例很多,可能会有很多相似代码,很自然想到用泛型来减少重复。这里必须在基类中泛子类的型,借助CRTP的方案如下:

template <typename Derived>
class Singleton {
   public:
    static Derived& GetInstance() {
        static Derived instance;
        return instance;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

   protected:
    Singleton() = default;
    ~Singleton() = default;
};

客户端代码如下:

#include <bits/stdc++.h>
using namespace std;

// Singleton.hpp
template <typename Derived>
class Singleton {
   public:
    static Derived& GetInstance() {
        static Derived instance;
        return instance;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

   protected:
    Singleton() { cout << "base init" << endl; };
    ~Singleton() { cout << "~base init" << endl; };
};

// MySingleton.hpp
// #include "Singleton.hpp"
class MySingleton : public Singleton<MySingleton> {
    friend Singleton<MySingleton>;

   public:
    void method() { cout << "my method!" << endl; }

   private:
    MySingleton() { cout << "my init" << endl; };
    ~MySingleton() { cout << "~my init" << endl; };
};

int main() {
    cout << "Enter main" << endl;
    MySingleton::GetInstance().method();
    cout << "Leave main" << endl;
}

Output:

Enter main
base init
my init
my method!
Leave main
~my init
~base init

下面是crtp版本单例模式的一些实现细节:

  1. 静态局部变量,静态入口函数。
  2. 为了避免有人无聊到用本例中的模版基类直接去new派生类,防止不必要的使用,比如Singleton<MySingleton> sm,模版基类的构造析构函数应该用protected来修饰。
  3. 基类Singleton析构函数非虚即可,理由详见[20.7] When should my destructor be virtual?Virtuality。一般来说,基类都有析构函数,且这个析构函数一般应该是个虚函数。什么情况下父类中可以没有析构函数或者析构函数可以不为虚呢?一、子类并没有析构函数的情况;二、代码中不会出现使用指向派生类对象的基类指针来删除对象的情况。
  4. 强制禁用拷贝构造函数和复制赋值运算符,而且最好是public=delete而不是private=delete。理由是Scott Meyers在Effective Modern C++中提到:Prefer deleted functions to private undefined ones,被禁用的函数通常应该是公开的,它会输出更合明确的错误消息,因为编译器会先检查可访问性,再检查是否弃置。从表达力看,delete蕴含的感情比private更强烈。private表示你不需要感知,而delete表示你被禁止使用。
  5. 子类不用再禁用一遍上面两个函数,理由是从C++11起在基类已经禁用这俩货的情况下子类也会自动弃置它们。这样客户端就又可以少写两行代码了。参见4 弃置的隐式声明的复制构造函数。如果满足下列任一条件,那么类T的隐式声明的复制构造函数被定义为弃置的(C++11起):T拥有无法复制的直接或虚基类(拥有被弃置、不可访问或有歧义的复制构造函数)。复制赋值运算符operator=同理,参考4 弃置的隐式声明的复制赋值运算符
  6. 子类和基类的构造析构函数能不能私有?分类讨论:
    • 基类保护+子类私有:父子权限不同可能有告警,否则首选。
    • 基类保护+子类保护:叶子类的析构函数一般不受保护,有点奇怪。
    • 基类保护+子类公共:可以省去友元,但子类权限没有最小化。
    • 基类私有:报错基类Singleton<MySingleton>构造器私有。

一个通用的准则是,任何基类的析构函数都必须是public and virtualprotected and non-virtual。—— cppreference

基于宏的代码生成技术

结合模版的代码生成技术后,单例模式已基本固定写法。另一种写法是基于macro的代码生成技术,以apollomodules/perception/common/algorithm/sensor_manager/sensor_manager.h为例,一个普通的SensorManager类经宏定义DECLARE_SINGLETON(SensorManager)修饰成为单例类。

class SensorManager {

  // other code

 private:
  DECLARE_SINGLETON(SensorManager)
};

该宏实现为:

#ifndef CYBER_COMMON_MACROS_H_
#define CYBER_COMMON_MACROS_H_

#include <iostream>
#include <memory>
#include <mutex>
#include <type_traits>
#include <utility>

#include "cyber/base/macros.h"

DEFINE_TYPE_TRAIT(HasShutdown, Shutdown)

template <typename T>
typename std::enable_if<HasShutdown<T>::value>::type
CallShutdown(T *instance) {
  instance->Shutdown();
}

template <typename T>
typename std::enable_if<!HasShutdown<T>::value>::type
CallShutdown(T *instance) {
  (void)instance;
}

// There must be many copy-paste versions of these macros which are
// same things, undefine them to avoid conflict.
#undef UNUSED
#undef DISALLOW_COPY_AND_ASSIGN

#define UNUSED(param) (void)param

#define DISALLOW_COPY_AND_ASSIGN(classname)                        \
  classname(const classname &) = delete;                           \
  classname &operator=(const classname &) = delete;

#define DECLARE_SINGLETON(classname)                               \
public:                                                            \
  static classname *Instance(bool create_if_needed = true) {       \
    static classname *instance = nullptr;                          \
    if (!instance && create_if_needed) {                           \
      static std::once_flag flag;                                  \
      std::call_once(flag, [&] {                                   \
        instance = new (std::nothrow) classname();                 \
      });                                                          \
    }                                                              \
    return instance;                                               \
  }                                                                \
                                                                   \
  static void CleanUp() {                                          \
    auto instance = Instance(false);                               \
    if (instance != nullptr) {                                     \
      CallShutdown(instance);                                      \
    }                                                              \
  }                                                                \
                                                                   \
private:                                                           \
  classname();                                                     \
  DISALLOW_COPY_AND_ASSIGN(classname)

#endif // CYBER_COMMON_MACROS_H_

总结一下单例模式的应用场景:

  1. 当一个类的多个实例可能没有意义,甚至可能造成混乱时。如:设备驱动;缓存;工厂方法模式中的工厂类、原型模式中的原型注册表。
  2. 当一个对象需要在整个系统中协调行动时。如:日志类;配置类、运行时配置、配置文件;串口管理,以共享模式访问资源。
  3. 当为了确定的初始化顺序而不得不封装已有的全局变量或全局状态。

参考资料

真实代码中的单例类

  1. opencv-highgui-win32 OpenCV內部的VideoBackendRegistry class為例,使用VideoBackendRegistry::getInstance()來取得實例,所以在整個程式裡VideoBackendRegistry這個class就只會有一份實例。
  2. aosp-platform_system_core-libutils AOSP(Android Open Source Project)裡面libutils提供的Singleton.h採用的是單一鎖方式,明顯地這種方式在多執行緒被頻繁呼叫的話會有race condition問題,2017年在程式碼裡改成禁用,並建議改用scoped static initialization的方式,也就是本文提到的Meyers Singleton實作方式。
  3. Kodi-PlayerCoreFactory.cpp 知名的xbmc專案(現已改名為Kodi)也是採用Meyers Singleton實作方式。
  4. 标准库里的std::clogstd::cerrstd::cinstd::cout也采用了单例模式。

关于单例模式是不是反模式

  1. yygq
  2. Why Singletons Are Controversial - googlecode
  3. 单例和单状态
  4. To Kill A Singleton
  5. So Singletons are bad, then what?
  6. Alternatives to Singletons and Global Variables
  7. Retiring_the_singleton_pattern.pdf
  8. The Clean Code Talks - “Global State and Singletons”
  9. Singleton Pattern Pitfalls
  10. Is Singleton Really Evil?

其他资料

  1. Singletons - loki6
  2. Thread-Safe Initialization of a Singleton
  3. Embedded C++: Singletons
  4. 誰也使用了單例模式?
  5. 浅谈设计模式六:单例模式 把dclp的微观顺序讲的很好
  6. C++ DP.07 Singleton
  7. 谈C++17里的Singleton模式
  8. 百度Apollo中的单例设计模式
  9. Double-Checked Locking Works in C++11