现代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就是基于这一思想的解决方案。

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

相关推荐
chao1898443 小时前
基于 SPEA2 的多目标优化算法 MATLAB 实现
开发语言·算法·matlab
赏金术士3 小时前
Kotlin 习题集 · 高级篇
android·开发语言·kotlin
楼兰公子4 小时前
buildroot 在编译rust时裁剪平台类型数量的方法
开发语言·后端·rust
知识领航员4 小时前
蘑兔AI音乐深度实测:功能拆解、实测表现与适用场景
java·c语言·c++·人工智能·python·算法·github
吴声子夜歌4 小时前
Go——并发编程
开发语言·后端·golang
ooseabiscuit5 小时前
Laravel4.x:现代PHP框架的奠基之作
java·开发语言·php
c1s2d3n4cs5 小时前
Qt模仿nlohmann::json进行序列化和反序列化
开发语言·qt·json
AiTop1006 小时前
Claude Code 推出 Agent View:命令行编程正式进入“多线程并发“时代
开发语言·人工智能·ai·aigc
jf加菲猫6 小时前
第21章 Qt WebEngine
开发语言·c++·qt·ui
码农-阿杰6 小时前
深入理解 synchronized 底层实现:从 HotSpot C++ 源码看对象锁与 Monitor 机制
开发语言·c++·