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,编译成功。

posted @ 2022-10-14 13:57  Leaos  阅读(179)  评论(0编辑  收藏  举报