现代C++ 实现单例模式

传统写法有什么问题

如果你了解过单例模式,双重检查锁定模式(Double-Checked Locking Pattern,后文简称DCLP)的写法你一定不会陌生,甚至你或许认为它是最正确的代码。

cpp 复制代码
class Singleton {
public:
	//获取单例
	Singleton* GetInstance() {
		//双重检查
		if (p_instance==nullptr) {
			_mutex.lock();
			if (p_instance == nullptr)
				p_instance = new Singleton();
			_mutex.unlock();
		}
		return p_instance;
	}
private:
	//私有构造函数
	Singleton() = default;
public:
	~Singleton() = default;
private:
	//删除构造方法
	Singleton& operator=(const Singleton& obj) = delete;
	Singleton(const Singleton& obj) = delete;
	Singleton& operator=(const Singleton&& obj) = delete;
	Singleton(const Singleton&& obj) = delete;
private:
	static Singleton* p_instance;
	mutex _mutex;
};
Singleton* Singleton::p_instance = nullptr;//类外初始化

这几乎是最常见的单例模式,不过上面的代码却有大问题

问题就出现在这一句:

cpp 复制代码
				p_instance = new Singleton();

该问题在Effective C++的作者Scott Meyers关于单例的论文中被深刻讨论,如果你之前对单例模式缺乏了解,这篇论文你一定需要阅读:

C++ and the Perils of Double-Checked Lockinghttps://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

其中提到,要执行该表达式 p_instance = new Singleton(); 需要做三件事:

  1. 开辟出用于存放单例的内存
  2. 在开辟的内存上构造Singleton对象
  3. 使p_instance指针指向开辟的内存

你期望,按照上述步骤去实例化单例,可是编译器没有任何约束去按你期望的顺序执行!

在特殊情况下,编译器有时会调换步骤二和步骤三,就像如下代码:

cpp 复制代码
	Singleton* GetInstance() {
		//双重检查
		if (p_instance==nullptr) {
			_mutex.lock();
			if (p_instance == nullptr) {
				p_instance = static_cast<Singleton*>	 //步骤三
					(operator new(sizeof(Singleton)));//步骤一
				new(p_instance) Singleton;			        //步骤二
			}
			_mutex.unlock();
		}
		return p_instance;
	}

此时执行顺序变为步骤一,步骤三,步骤二

如果你还没有理解,设想有如下场景:

  • 线程A 进入GetInstance,执行完判断,上锁,执行步骤三和步骤一。此时p_instance已经是一个非空的指针,但Singleton单例还未在p_instance所指向的空间上初始化。
  • 线程B 进入GetInstance,判断p_instance为非空,随后把p_instance返回。调用处对p_instance解引用,直接G!访问未初始化的内存。

DCLP只会在步骤一,步骤二在步骤三之前执行才能奏效,但他们的执行顺序确是未定义行为!编译器在这一点上不受任何约束,完全可能导致意想不到的问题。

**所以用DCLP实现的单例模式,单例对象的初始化顺序不确定。**这种情况可能导致在一个线程中访问尚未初始化的单例对象,从而引发错误。 并不是线程安全,因为包含未定义行为。

现代C++实现单例模式

为了规避这个问题,可利用C++11引入 魔法静态变量 特性,主要描述了具有静态存储期或线程存储期的块作用域变量的初始化规则。

§6.7 [stmt.dcl] p4原文: If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

即,如果在变量(具有静态存储期或线程存储期的块作用域的)初始化过程中同时并发地再次进入声明,那么并发执行必须等待初始化过程完成。
§6.7 [stmt.dcl] p4http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

利用这项标准,你就可以很爽的写出比DCLP更好的代码!

代码样例:

cpp 复制代码
class Singleton {
public:
	//现代写法
	Singleton& GetInstance() {
		// Let's create a magic static
		static Singleton instance;//C++11
		return instance;//return refence
	}
private:
	//私有构造函数
	Singleton() = default;
public:
	~Singleton() = default;
private:
	//删除构造方法
	Singleton& operator=(const Singleton& obj) = delete;
	Singleton(const Singleton& obj) = delete;
	Singleton& operator=(const Singleton&& obj) = delete;
	Singleton(const Singleton&& obj) = delete;
};

现代写法使用使用局部静态变量来实现单例模式。具体而言,文章提出的Meyers' Singleton就是基于这一思想的解决方案。

如果本文帮助到你,希望能点赞收藏,欢迎留言讨论。

相关推荐
古希腊掌管学习的神几秒前
[LeetCode-Python版]相向双指针——611. 有效三角形的个数
开发语言·python·leetcode
赵钰老师1 分钟前
【R语言遥感技术】“R+遥感”的水环境综合评价方法
开发语言·数据分析·r语言
雨中rain8 分钟前
Linux -- 从抢票逻辑理解线程互斥
linux·运维·c++
就爱学编程9 分钟前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
Oneforlove_twoforjob32 分钟前
【Java基础面试题025】什么是Java的Integer缓存池?
java·开发语言·缓存
emoji11111133 分钟前
前端对页面数据进行缓存
开发语言·前端·javascript
每天都要学信号44 分钟前
Python(第一天)
开发语言·python
TENET信条44 分钟前
day53 第十一章:图论part04
开发语言·c#·图论
生信圆桌1 小时前
【生信圆桌x教程系列】如何安装 seurat V5版本R包,最详细安装手册
开发语言·r语言
IT猿手1 小时前
最新高性能多目标优化算法:多目标麋鹿优化算法(MOEHO)求解TP1-TP10及工程应用---盘式制动器设计,提供完整MATLAB代码
开发语言·深度学习·算法·机器学习·matlab·多目标算法