0%

单例模式(Singleton):使用与实现

Singleton是GoF Design Patterns中提及的面向对象的23种设计模式之一,书中关于Singleton的定义如下

Ensure a class only has one instance, and provide a global point of access to it

实践中,一般Singleton被认为是:

  • 在程序的整个寿命周期中,只能拥有一个实例的类
  • 提供一个在全局能够访问该唯一实例的方法

本文解决以下问题:

  • 为什么我们需要Singleton
  • “Magic Static” 与 libsupc++ 中 “Magic Static” 的实现
  • 如何使用 “Magic Static” 实现 Singleton
  • Singleton 有哪些不足

为什么我们需要 Singleton

GoF Design Patterns中这样描述何时使用 Singleton:

Use the Singleton pattern when:
• there must be exactly one instance of a class, and it must be accessible to clients from a well-known access point.
• when the sole instance should be extensible by subclassing, and clients should be able to use an extended instance without modifying their code.

当我们要在一个复杂系统中实现一个新特性时。与其在层层包装的复杂系统中以组合的形式添加新模块,不如将新模块实现成一个全局可达的Singleton。

Singleton 的实现

Singleton在程序的生命周期中只允许存在一个实例,因此我们需要确保:

  • 在第一次使用Singleton的方法时,该实例已经被正确地初始化。
  • 每次使用Singleton的方法,都作用在同一实例上。
  • 在多线程情况下,该实例的初始化不出现race condition。

Meyers Singleton通过C++11标准中对静态局部对象的初始化来保证以上三个条件。然而当程序中存在多个相互依赖的Singleton时,Singleton的初始化顺序需要得到妥善处理,因此产生了按需初始化的Singleton

Magic Static

C++ 11标准中规定了“具有静态存储期,且只初始化一次的块作用域变量”。我们一般称定义于函数内的,并将其返回给函数调用者的local static变量为”Magic Static”。”Magic Static”是实现Singleton的基石。

标准中§8.8 stmt.dcl关于静态局部变量的初始化提到:

Dynamic initialization of a block-scope variable with static storage duration or thread storage duration is performed 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.85 If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.

声明于块作用域且带有 staticthread_local (C++11 起) 说明符的变量拥有静态或线程 (C++11 起)存储期,但在控制首次经过其声明时才会被初始化(除非其初始化是零初始化或常量初始化,这可以在首次进入块前进行)。在其后所有的调用中,声明均被跳过。

  • 若初始化抛出异常,则不认为变量被初始化,且控制下次经过该声明时将再次尝试初始化。
  • 若初始化递归地进入正在初始化的变量的块,则行为未定义。
  • 若多个线程试图同时初始化同一静态局部变量,则初始化严格发生一次(类似的行为也可对任意函数以 std::call_once 来达成)。注意:此功能特性的通常实现均使用双检查锁定模式的变体,这使得对已初始化的局部静态变量检查的运行时开销减少为单次非原子的布尔比较
  • 块作用域静态变量的析构函数在初始化已成功的情况下在程序退出时被调用。
  • 相同内联函数(可以是隐式内联)的所有定义中,函数局域的静态对象均指代定义于一个翻译单元中的同一对象。

在上面这段代码中,local static变量A就是就是一个”Magic Static”,编译器为了保证该变量在在控制首次经过其声明时才会被初始化,使用了一个guard variable: guard variable for init_A()::a,并调用了__cxa_guard_acquire__cxa_guard_release。在libstdc++libsups++中,其在x86-64 Linux环境下实现大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
if (__guard.first_byte == 0) {
if ( __cxa_guard_acquire (&__guard) ) {
try {
// ... initialize the object ...;
} catch (...) {
__cxa_guard_abort (&__guard);
throw;
}
// ... queue object destructor with __cxa_atexit() ...;
__cxa_guard_release (&__guard);
}
}

其实现大致为:使用一个原子变量__guard作为互斥锁,对__guard进行争夺,争夺成功的线程进行初始化,争夺失败的线程使用futex来放入内核的等待队列,等待该变量成功初始化。


对于每个local static变量都会生成一个__guard,来标示其初始化的状态:

1
__extension__ typedef int __guard __attribute__((mode (__DI__)));

其中__attribute__((mode (__DI__)))是gcc的vector extensions

DI,An integer, eight times as wide as a QI mode integer, usually 64 bits.

__guard是一个64bit的整数,其可能的状态如下:

1
2
3
4
5
6
7
8
9
10
11
// Valid values of the first integer in guard are:
// 0 No thread encountered the guarded init
// yet or it has been aborted.
// _GLIBCXX_GUARD_BIT The guarded static var has been successfully
// initialized.
// _GLIBCXX_GUARD_PENDING_BIT The guarded static var is being initialized
// and no other thread is waiting for its
// initialization.
// (_GLIBCXX_GUARD_PENDING_BIT The guarded static var is being initialized
// | _GLIBCXX_GUARD_WAITING_BIT) and some other threads are waiting until
// it is initialized.

__cxa_guard_acquire__guard使用__atomic_compare_exchange_n进行CAS,如果CAS成功,则返回1,否则返回0,并将gi的值拷贝进expected

  • CAS成功,即__guard为0,且成功将__guard变为_GLIBCXX_GUARD_PENDING_BIT,则本线程负责该变量的初始化。
  • CAS失败:
    • __guard_GLIBCXX_GUARD_BIT,即已经成功初始化,返回0
      -__guard_GLIBCXX_GUARD_PENDING_BIT,即有线程正在初始化,当前线程应该等待正在进行初始化的线程成功完成初始化,使用futex来将该线程放入内核中的等待队列。

__cxa_guard_abort用于初始化失败抛出异常的处理。初始化失败时,将__guard的flag变为0。如果之前__guard_GLIBCXX_GUARD_WAITING_BIT,即有线程在futex的内核等待队列上等待初始化完成,则将等待的全部线程唤醒。被唤醒的全部线程会重新尝试竞争这个初始化锁,并重新尝试初始化该变量。(可能的惊群?因为只有一个线程可以对该变量进行初始化)


__cxa_guard_release用于初始化成功时,将__gurad变为_GLIBCXX_GUARD_BIT,并唤醒所有在futex的内核等待队列上等待的线程。

Meyers Singleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class singleton
{
public:
static singleton& instance()
{
static singleton s{}; // #1
return s;
}

singleton(const singleton&) = delete; // #2
void operator=(const singleton&) = delete;
private:
singleton()
{
// ...
};
};
  • #1处使用静态局部变量来保证该singleton的实例在首次调用singleton::instance()时被初始化,只初始化一次,且不产生race condition。
  • 根据Effetive Modern C++.pdf)中Item11: Prefer deleted functions to private undefined one. :#2处将copy ctor 和 copy assignment operator使用=delete删除,并声明为public。相比较将其声明为private并做不定义,声明成publicdelete有以下优势:
    • 可以保证即使在该类的成员函数友元函数也无法进行拷贝复制。
    • C++在编译时先检查访问权限再检查delete,因此声明为public可以获得更明确的错误信息。
  • 无需考虑删除 move ctor 和 move assignment operator,因为它们只在一个类中不含有用户定义的copy operations, move operations 和 dtor 时才会被编译器生成。

按需初始化和释放的 Singleton

当各个 Singleton之间有依赖关系时,需要保证各个 Singleton之间的初始化和释放的顺序。尝试使用unique_ptr配合IIFE(immediately invoked function expression)来完成local static的初始化和释放。

Singleton 有哪些问题

Singletons之间的初始化依赖

Singleton往往被实现为local static对象。C++只保证local static对象在其所在的函数第一次被调用时完成初始化。当这些Singleton之间相互依赖时,它们的初始化顺序无法被很好地保证。当你的设计中存在多个Singleton时,可能需要使用一个Singleton Manager和按需初始化/释放的Singleton,来保证各个Singleton之间有序初始化。

难以进行单元测试

Singleton作为一个全局对象,其生命周期与整个程序大致相同。这导致所有依赖Singleton的功能的单元测试十分困难。其次,大部分的单元测试框架依赖于Singleton,单元测试框架的Singletons和被测试的程序的Singletons之间的初始化顺序难以保证,可能会出现意想不到的问题。

多线程

Singleton作为一个全局可达的资源,多线程程序修改Singleton时必须要进行正确的同步,这可能导致程序的性能下降。


参考

Magic Static - mbedded.ninja
Adventures in Systems Programming: C++ Local Statics - Manish Goregaokar’s blog
Singletons - vladris.com