多线程 - 单例设计模式中的多线程
大约 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。但此时构造器还未被调用,会出错
- 当A线程执行
- 建议
- 不要用,不安全,容易出错
版本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对象,自己又另外创建了一个)