Muduo库之WeakCallback、Singleton
WeakCallback
在 WeakCallback.h
文件中定义了模板类 WeakCallback
,在其模板参数中,有一个可变模板参数 ARGS
,用以指示回调函数的参数。
在类内部,定义有两个成员变量,分别是 object_
和 function_
。
成员变量 object_
是一个弱指针类型,即 weak_ptr
。其目的是为了实现一种弱回调机制,使用 weak_ptr 将其绑定到回调函数中,这样对象的生命周期就不会被延长。同时,在调用回调函数时,尝试将其提升为 shared_ptr
,如果提升成功,那么说明该回调函数还健在,那么就执行回调;如果提升失败,则无法调用回调函数。这样就可以设计出线程安全的回调函数。
另一个成员变量 function_
是一个不定参的函数对象,即 std::function
。在 C++11 之前,我们需要使用函数指针来定义回调函数,而在 std::function
关键字出现后,我们就可以使用这种更为方便的方式来定义回调函数。它是一个函数包装器模板,最早来自于 boost 库。它可以指向任何函数、函数指针、成员函数、静态函数、lambda 表达式和函数对象均可。
在类成员的内部,重载了 ()
运算符:
void operator()(ARGS&&... args) const {
std::shared_ptr<CLASS> ptr(object_.lock());
if (ptr) {
function_(ptr.get(), std::forward<ARGS>(args)...);
}
}
其中,调用 object_ 变量的 lock()
方法尝试将其提升为强指针 shared_ptr
,然后检查其有效性,判断该回调函数是否还健在,如果健在,则调用该回调函数,同时使用 std::forward
进行完美转发。完美转发是指在函数模板中,完全按照模板的参数的类型,即保持参数的左值、右值特征,将参数传递给函数模板中的另一个函数。
在类外,则定义了供常量对象和非常量对象使用的 makeWeakCallback()
函数,以返回 WeakCallback
对象。
Singleton
单例模式的实现
Singleton.h
主要实现了单例模式相关类。常见单例模式的实现主要分为以下几种:
懒汉式
懒汉式要求先声明单例对象,然后在调用时才完成实例化操作。
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;
这种写法并没有考虑到多线程的情况,因此在多线程情况下可能产生多个实例对象,违背单例原则。
双检锁
为了解决 “懒汉式” 中存在的多线程问题,我们可以通过互斥锁来避免多个实例的创建。
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 出来对象后,第二个线程就不用竞争第一个线程的对象锁而进行等待;
- 第二层判空是为了保证线程安全,防止多次实例化操作;
但是,如果该单例对象比较大,那么加锁操作就会成为一个性能瓶颈。
饿汉式
为了解决双检锁所存在的性能瓶颈问题,设计出了 “饿汉式” 的单例模式。饿汉式则要求单例对象的声明和实例化同时完成。
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();
因为单例对象的静态初始化是在程序开始之前,在静态资源区中已经初始化了实例对象,所以静态初始化也就保证了线程安全性。在性能要求较高时,可以采用这种方式,从而避免了频繁的加锁、解锁操作造成的资源浪费。
静态内部类
但是如果单例对象无需考虑销毁操作,单例对象的生命周期伴随着整个程序的生命周期,程序结束时,由操作系统自动回收资源。那么如果无需考虑销毁操作,则可以用静态内部类的方式进行实现:
class Signleton {
public:
static Signleton* getInstance() {
static Signleton instance;
return &instance;
}
private:
Signleton() {}
};
Singleton实现
在 muduo
的实现中,模板类 Singleton
的内部定义有两个静态成员变量:ponce_
和 value_
。其中,前者是 pthread_once_t
类型,以保证单例且线程安全。后者则是一个指针类型,指向单例对象。它们的初始化操作如下:
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;
template<typename T>
T* Singleton<T>::value_ = NULL;
类中的获取实例及初始化函数如下:
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)
机制,即 “匹配失败不是错误”。具体来说,就是当重载的模板参数展开时,如果展开导致一些类型不匹配,编译器并不报错。而正好可以利用该机制来判断类是否存在某个成员函数。
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;
};
在 SFINEA
机制中,编译期会优先匹配最合适的函数,匹配失败后会找寻次一级的匹配函数,直到无法匹配到任何函数。而在如上代码中,调用静态变量 value
即可返回是否存在 no_destory()
函数的结果,而 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()
函数如下:
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 是否为不完全类型。
如果是不完全类型,那么 sizeof(T) == 0
为真,数组大小为 -1,编译错误;如果是完全类型,那么 sizeof(T) == 0
为假,数组大小为 1,编译成功。