muduo库-Singleton类

muduo库-Singleton类

Singleton类的实现

常见的单例模式实现主要分为以下几种:

懒汉式

懒汉式要求先声明单例对象,然后在调用时才完成实例化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Signleton {
public:
static Signleton* getInstance() {
if (instance == nullptr) {
instance = new Signleton();
}
return instance;
}

static void destory() {
if (instance != nullptr) {
delete instance;
instance = nullptr;
}
}
private:
Signleton() {}
static Signleton* instance;
};

Signleton* Signleton::instance = nullptr;

这种写法并没有考虑到多线程的情况,因此在多线程情况下可能产生多个实例对象,违背单例原则。

双检锁

为了解决 “懒汉式” 中存在的多线程问题,我们可以通过互斥锁来避免多个实例的创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Signleton {
public:
static Signleton* getInstance() {
if (instance == nullptr) {
mutex.lock();
if(instance == nullptr) {
instance = new Signleton();
}
mutex.unlock();
}
return instance;
}

static void destory() {
if (instance != nullptr) {
delete instance;
instance = nullptr;
}
}
private:
Signleton() {}
static Signleton* instance;
static pthread_mutex_t mutex;
};

Signleton* Signleton::instance = nullptr;
pthread_mutex_t Signleton::mutex = PTHREAD_MUTEX_INITIALIZER;

进行两次判断以避免多次加锁和解锁操作,保证线程安全。这两次判空的意义如下:

  • 第一层判空是为了提高效率,即当有一个线程 new 出来对象后,第二个线程就不用竞争第一个线程的对象锁而进行等待;
  • 第二层判空是为了保证线程安全,防止多次实例化操作;

但是,如果该单例对象比较大,那么加锁操作就会成为一个性能瓶颈。

饿汉式

为了解决双检锁所存在的性能瓶颈问题,设计出了 “饿汉式” 的单例模式。饿汉式则要求单例对象的声明和实例化同时完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Signleton {
public:
static Signleton* getInstance() {
return instance;
}

static void destory() {
if (instance != nullptr) {
delete instance;
instance = nullptr;
}
}
private:
Signleton() {}
static Signleton* instance;
};

Signleton* Signleton::instance = new Signleton();

因为单例对象的静态初始化是在程序开始之前,在静态资源区中已经初始化了实例对象,所以静态初始化也就保证了线程安全性。在性能要求较高时,可以采用这种方式,从而避免了频繁的加锁、解锁操作造成的资源浪费。

静态内部类

但是如果单例对象无需考虑销毁操作,单例对象的生命周期伴随着整个程序的生命周期,程序结束时,由操作系统自动回收资源。那么如果无需考虑销毁操作,则可以用静态内部类的方式进行实现:

1
2
3
4
5
6
7
8
9
class Signleton {
public:
static Signleton* getInstance() {
static Signleton instance;
return &instance;
}
private:
Signleton() {}
};

muduo库的Singleton类实现

其类图如下图所示:

在 muduo 的实现中,模板类 Singleton 的内部定义有两个静态成员变量:ponce_value_。其中,前者是 pthread_once_t 类型,以保证单例且线程安全。后者则是一个指针类型,指向单例对象。它们的初始化操作如下:

1
2
3
4
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;
template<typename T>
T* Singleton<T>::value_ = NULL;

类中的获取实例及初始化函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
static T& instance() {
pthread_once(&ponce_, &Singleton::init);
assert(value_ != NULL);
return *value_;
}

static void init() {
value_ = new T();
if (!detail::has_no_destroy<T>::value){
::atexit(destroy);
}
}

其中,pthread_once() 保证 init() 函数只调用一次,避免多线程竞争,保证了线程安全。而 has_no_destroy<T> 则是为了判断该类型是否含有 no_destory() 函数,如果不存在,则调用 atexit()注册 destroy() 函数,当程序正常终结时,调用指定的 destroy() 函数以回收资源。

其中的 has_no_destory<T> 定义如下,这是利用了 C++ 中的 SFINEA(Substitution failure is not an error) 机制,即 “匹配失败不是错误”。具体来说,就是当重载的模板参数展开时,如果展开导致一些类型不匹配,编译器并不报错。而正好可以利用该机制来判断类是否存在某个成员函数。

1
2
3
4
5
6
template<typename T>
struct has_no_destroy {
template <typename C> static char test(decltype(&C::no_destroy));
template <typename C> static int32_t test(...);
const static bool value = sizeof(test<T>(0)) == 1;
};

假如类中存在 no_destory() 函数,那么 decltype(&C::no_destory) 表达式会返回一个函数指针,该指针指向类中的 no_destory() 函数。此时 test<T>(0) 就会匹配为 char test() 函数,并返回 char,由于 sizeof(char)1,所以 sizeof(test<T>(0) == 1 表达式会返回 true,表示存在该函数。

假如类中不存在该函数,那么在匹配函数 char test() 时就会匹配错误,进而选择次一级的匹配选项,即 int32_t test(...) 函数,由于该函数参数中为可变参数,所以可以接受任意类型的函数参数,test<T>(0) 与该函数匹配成功,进而返回 int32_t。最后由于 sizeof(int32_t) == 1 返回 false,则表示不存在该函数。

该类中的 destory() 函数如下:

1
2
3
4
5
6
static void destroy() {
typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
T_must_be_complete_type dummy; (void) dummy;
delete value_;
value_ = NULL;
}

其中使用 typedef 关键字定义了一个数组类型,用于在编译期判断类型 T 是否是不完全类型,不完全类型指的是只有声明却没有定义的类,那么不完全类型在 delete 操作时也就无法调用析构函数,因此在 delete value_ 操作之前需要判断类型 T 是否为不完全类型。

muduo的单例模式采用模板类实现,它内部维护一个模板参数的指针,可以生成任何一个模板参数的单例。凭借SFINAE技术muduo库可以检测模板参数如果是类的话,并且该类注册了一个no_destroy()方法,那么muduo库不会去自动销毁它。否则muduo库会在init时,利用pthread_once()函数为模板参数,注册一个atexit时的destroy()垃圾回收方法,实现自动垃圾回收。智能指针也能达到类似的效果,我们平时写的单例模式在Singleton中写一个Garbage类也可以完成垃圾回收。

如果是不完全类型,那么 sizeof(T) == 0 为真,数组大小为 -1,编译错误;如果是完全类型,那么 sizeof(T) == 0 为假,数组大小为 1,编译成功。

总结

Singleton类的整体结构如下: