跳至主要內容

多线程 - 单例设计模式中的多线程

LincZero大约 5 分钟

多线程 - 单例设计模式中的多线程

单例设计模式简概

设计模式简概

(详见设计模式相关的笔记)

单例模式例程

(详见设计模式相关的笔记)

在设计模式中,使用频率比较高

class Single
{
private:
    Single(){}
    static Single *m_instance;	// 静态成员变量
public:
    static Single * Instance()
    {
        if(m_instance == nullptr)
        {
            m_instance = new Single();	// new了
            static GarbageCollector gc;	// 创建回收类
        }
        return m_instance;
    };
    
    // 不写这个也行,一般单例对象的生命周期就是覆盖整个程序的,退出程序自动析构
    class GarbageCollector				// 内部类,负责释放前面的new
    {
        ~GarbageCollector()
        {
            if(Single::m_instance)
            {
                delete Single::m_instance;
                Single::m_instance = nullptr;
            }
        }
    };
}
Single *Single::m_instance = nullptr;	// 初始化为空

int main
{
	Single *single = Single::Instance();	// 使用单例对象
    return 0;
}
  • Q:为什么不能在单例类的析构中进行delete?
  • A:当析构Single对象时,此时m_instance指针可能已经被释放了。如果此时析构函数中delete m_instance;,有可能会释放该内存两次?出错 还是因为自己delete自己会造成逻辑混乱? 其实一般情况下也不用手动析构,一般单例对象的生命周期就是覆盖整个程序的,退出程序自动析构

单例设计模式共享数据问题的分析、解决

存在问题

(详见设计模式相关的笔记)

单例模式若需要多线程下起作用(多个线程共用一个单例),则需要Instance()函数互斥

版本1:线程非安全

(可能多个线程时能够创建不止一个单例,有几率出错,不安全)

(出错原因和改进方法详见设计模式单例模式的笔记)

  • 存在问题
    • 在多线程中,例如AB两个线程
    • A执行完if(判断单例是否存在)还没执行下一行时,B恰好执行到if那行
    • 这就会产生两个单例
  • 建议
    • 单线程可以用
class Single
{
private:
    Single(){}
    static Single *m_instance;				// 静态成员变量
public:
    static Single * Instance()
    {
        if(m_instance == nullptr)
        {
            m_instance = new Single();		// new了
        }
        return m_instance;
    };
}
Single *Single::m_instance = nullptr;		// 初始化为空

// 线程入口函数
void mythread()
{
    cout << "子线程" << endl;
    Single *single = Single::Instance();	// 使用单例对象
    return;
}

int main
{
	std::thread mytobj(mythread);
    return 0;
}

版本2:线程安全 - 加锁版(Cleck Lock

  • 存在问题:
    • 锁的代价过高
    • 读的时候根本没必要去锁,性能浪费
  • 建议
    • 可以用,但高并发的话性能损耗大
#include <mutex>
using namespace std;

std::mutex mutex1;							// 【互斥量】

class Single
{
private:
    Single(){}
    static Single *m_instance;				// 静态成员变量
public:
    static Single * Instance()
    {
        std::unique_lock<std::mutex> up_mutex(mutex1)	// 【互斥量加锁】
        if(m_instance == nullptr)
        {
            m_instance = new Single();		// new了
        }
        return m_instance;
    };
}
Single *Single::m_instance = nullptr;		// 初始化为空

// 线程入口函数
void mythread()
{
    cout << "子线程" << endl;
    Single *single = Single::Instance();	// 使用单例对象
    return;
}

int main
{
	std::thread mytobj(mythread);
    return 0;
}

版本3:线程安全 - 双检查锁(Double Cleck Lock

  • 存在问题
    • 由于内存读写reorder不安全,会导致双检查锁失效
    • reorder:从汇编指令的角度来看,m_instance = new Singleton();这行代码可以分解成三个步骤
      • (1) 分配内存
      • (2) 调用构造器
      • (3) 地址返回值给指针
      • 但这三步只是理想指令执行顺序,实际情况中有可能 被reorder(重排顺序),顺序变成了 1-3-2
    • 那么假设存在AB两个线程并发生以下情况
      • 当A线程执行m_instance = new Singleton();时被reorder,即按1-3-2顺序执行指令
      • 当A线程执行完指令3但还没执行指令2时
      • B恰好依次执行代码第2行的判断、第8行的return。但此时构造器还未被调用,会出错
  • 建议
    • 不要用,不安全,容易出错

版本4:C++11的 volatile 关键字

  • 简概
    • C++11引入的新函数
  • 功能
    • 原理主要是让编译器不reorder

(函数略)

版本5:C++11的 std::call_once()

  • 简概
    • C++11引入的新函数,感觉这个函数简直是为单例模式量身定做的
  • 功能
    • 能保证某个函数只被调用一次
    • 能实现互斥量的功能,且效率上会更高
    • 与if-else相比,在多线程中使用会更安全
  • 原理:
    • 第二个参数是需要调用的函数名
    • 第一个参数是一个std::once_flag类型的标记。该标记将决定函数是否执行。 当执行过一次call_once()后,就会把这个标记设置为已调用状态。后续再调用时就会无法调用
using namespace std;

std::once_flag g_flag;						// 【标记】这是个标记

class Single
{
private:
    Single(){}
    static Single *m_instance;				// 静态成员变量
    static void CreateInstance()			// 创建实例【只被调用一次】
    {
        m_instance = new Single;
    }
public:
    static Single * Instance()				// 使用实例
    {
        std:call_once(g_flage, CreateInstance);	// 【call_once方法】传入标记、函数名作参
        return m_instance;
    };
}
Single *Single::m_instance = nullptr;		// 初始化为空

// 线程入口函数
void mythread()
{
    cout << "子线程" << endl;
    Single *single = Single::Instance();	// 使用单例对象
    return;
}

int main
{
    Single::Instance();						// 【在主线程中初始化创建】那么new出来的对象就会在主线程的堆栈空间中,更好
	std::thread mytobj(mythread);
    return 0;
}

版本6:直接使用static的简易版本

C++11中还有一种简单的单例,就是直接使用static。

C++11中可以保证static变量时多线程安全的,在底层实现了加锁操作,而且由于是static对象,也可以保证对象只生成一次

但用这种写法的基类可能因使用时操作不当不安全(比如不用static对象,自己又另外创建了一个)