使用单例模式进行多线程编程

[[单例模式]]简而言之就是程序中的某个类只能实例化一个对象。因为对象只有一个,在不同线程中实例化的时候,实际上都是获得那个唯一对象的引用或者指针,那么如果通过这个唯一的对象去共享数据,就可以实现多线程中的数据共享。

单例模式类的设计

首先看一下 [[Cpp]] 中如何实现单例模式的类的设计

class MyCAS {
 private:
  MyCAS() {}                                 // 构造函数私有,禁止通过普通的方式实例化
 
 private:
  static MyCAS* m_instance;

 public:
  static MyCAS* Getinstance() {
    if (m_instance == NULL) {
      m_instance = new MyCAS();
      static CGarhuishou cl;                 // 生命周期一直到程序退出
    }
    return m_instance;
  }
  class CGarhuishou {                        // 用于管理单例对象资源的类
   public:
    ~CGarhuishou() {
      if (MyCAS::m_instance) {
        delete MyCAS::m_instance;
        MyCAS::m_instance = NULL;
      }
    }
  };
  void func() { cout << "测试" << endl; }      // 其他成员函数
};

// 在源码文件中对静态成员变量进行定义和初始化
MyCAS* MyCAS::m_instance=NULL;                // 类的静态成员变量定义和初始化

注意到该类的构造函数是用 private 修饰的,这样就不能创建基于该类的对象了。例如下面的代码都将无法编译通过,这正是单例类要达到的效果。

MyCAS a1;                 // 非法
MyCAS *pa = new MyCAS();  // 非法

只能通过以下方式实例化对象:

MyCAS *p_a = MyCAS::Getinstance();

这里就创建了一个 MyCAS 对象的指针。
另外,由 CGarhuishou 类来管理 m_instance 指针所指向资源的回收。

单例模式应用并发编程

首先建议单例模式的类在所有子线程启动前实例化,这样可以避免一些多线程访问对象资源冲突的问题。
如果我们不这样做,可能会发生什么?想象一下,子线程 1 和子线程 2 中同时进行实例化,当子线程 1 在执行了

if (m_instance == NULL)

时,程序切换到了子线程 2,此时子线程 2 也执行到了

if (m_instance == NULL)

这样,两个线程都会认为 m_instance == NULLtrue,那么还是会执行两次

m_instance = new MyCAS();

为了解决这个问题,可以使用互斥锁+双重检查的方式。

static MyCAS* Getinstance() {
  if (m_instance == NULL) {
    std::unique_lock<std::mutex> mymutex(resource_mutex);
    if (m_instance == NULL) {
      m_instance = new MyCAS();
      static CGarhuishou cl;  // 生命周期一直到程序退出
    }
  }
  return m_instance;
}

因为在多线程中 m_instance == NULL 成立不一定代表唯一执行 m_instance = new MyCAS();,很有可能切换线程了。但是使用互斥锁就能在几个线程同时 new 的时候只有一个继续往下实例化,而当实例化完后释放互斥锁,其他线程再一次检查 m_instance == NULL 时就已经是 FALSE 了。

另一种写法

使用 std::call_once 保证函数只被调用一次。假设有个函数,名字为 acall_once 的功能就是能够保证函数 a 只被调用一次。读者都知道,例如有两个线程都调用函数 a,那么这个函数 a 肯定是会被调用两次。但是,有了 call_once,就能保证,即便是在多线程下,这个函数 a 也只会被调用一次。
代码实现:

std::once_flag g_flag;         //这是一个系统定义的标记

实际上 call_once 就是控制这个 flag 来实现,函数只能被单次调用。
另外在 MyCAS 里面再新增一个函数

private:
 static void CreateInstance(){
  m_instance = new MyCAS();
  static CGarhuishou cl;
 }

并修改 Getinstance() 函数

static MyCAS* Getinstance() {
  if (m_instance == NULL) {
   std::call_once(g_flag, CreateInstance);
  }
  return m_instance;
}

这样,即使是两个线程都判断 m_instance == NULL 成立并进入内部作用域后,CreateInstance 仍然只会执行一次,确保只会实例化一个对象。

Reference

[[C++新经典]]

posted @ 2023-10-19 15:27  pomolnc  阅读(26)  评论(0编辑  收藏  举报