如何使用C++写一个线程安全的单例模式?

本文探讨了在多线程环境中如何实现线程安全的单例模式,从简单实现到存在问题的双重检测锁,再到现代C++提供的解决方案,包括使用内存顺序限制、call_once函数和静态局部变量。每种方法的优缺点和其实现细节都进行了分析,旨在提供高效且线程安全的单例模式实现方案。

如何写一个线程安全的单例模式?

单例模式的简单实现

单例模式大概是流传最为广泛的设计模式之一了。一份简单的实现代码大概是下面这个样子的:

class singleton
{
public:
	static singleton* instance()
	{
		if (inst_ != nullptr) { 
			inst_ = new singleton();
		}
		return inst_;
	}
private:
	singleton(){}
	static singleton* inst_;
};

singleton* singleton::inst_ = nullptr;

这份代码在单线程的环境下是完全没有问题的,但到了多线程的世界里,情况就有一点不同了。考虑以下执行顺序:

  1. 线程1执行完if (inst_ != nullptr)之后,挂起了;
  2. 线程2执行instance函数:由于inst_还未被赋值,程序会inst_ = new singleton()语句;
  3. 线程1恢复,inst_ = new singleton()语句再次被执行,单例句柄被多次创建。

所以,这样的实现是线程不安全的。

有问题的双重检测锁

解决多线程的问题,最常用的方法就是加锁呗。于是很容易就可以得到以下的实现版本:

class singleton
{
public:
	static singleton* instance()
	{
		guard<mutex> lock{ mut_ };
		if (inst_ != nullptr) {
			inst_ = new singleton();
		}
		return inst_;
	}
private:
	singleton(){}
	static singleton* inst_;
	static mutex mut_;
};

singleton* singleton::inst_ = nullptr;
mutex singleton::mut_;

这样问题是解决了,但性能上就不那么另人满意,毕竟每一次使用instance都多了一次加锁和解锁的开销。更关键的是,这个锁也不是每次都需要啊!实际我们只有在创建单例实例的时候才需要加锁,之后使用的时候是完全不需要锁的。于是,有人提出了一种双重检测锁的写法:

...
	static singleton* instance()
	{
		if (inst_ != nullptr) {
			guard<mutex> lock{ mut_ };
			if (inst_ != nullptr) {
				inst_ = new singleton();
			}
		}
		return inst_;
	}
...

我们先判断一下inst_是否已经初始化了,如果没有,再进行加锁初始化流程。这样,虽然代码看上去有点怪异,但好像确实达到了只在创建单例时才引入锁开销的目的。不过遗憾的是,这个方法是有问题的。Scott Meyers 和 Andrei Alexandrescu 两位大神在C++ and the Perils of Double-Checked Locking 一文中对这个问题进行了非常详细地讨论,我们在这儿只作一个简单的说明,问题出在:

	inst_ = new singleton();

这一行。这句代码不是原子的,它通常分为以下三步:

  1. 调用operator new为singleton对象分配内存空间;
  2. 在分配好的内存空间上调用singleton的构造函数;
  3. 将分配的内存空间地址赋值给inst_。

如果程序能严格按照1-->2-->3的步骤执行代码,那么上述方法没有问题,但实际情况并非如此。编译器对指令的优化重排、CPU指令的乱序执行(具体示例可参考《【多线程那些事儿】多线程的执行顺序如你预期吗?》)都有可能使步骤3执行早于步骤2。考虑以下的执行顺序:

  1. 线程1按步骤1-->3-->2的顺序执行,且在执行完步骤1,3之后被挂起了;
  2. 线程2执行instance函数获取单例句柄,进行进一步操作。

由于inst_在线程1中已经被赋值,所以在线程2中可以获取到一个非空的inst_实例,并继续进行操作。但实际上单例对像的创建还没有完成,此时进行任何的操作都是未定义的。

现代C++中的解决方法

在现代C++中,我们可以通过以下几种方法来实现一个即线程安全、又高效的单例模式。

使用现代C++中的内存顺序限制

现代C++规定了6种内存执行顺序。合理的利用内存顺序限制,即可避免代码指令重排。一个可行的实现如下:

class singleton {
public:
	static singleton* instance()
	{
		singleton* ptr = inst_.load(memory_order_acquire);
		if (ptr == nullptr) {
			lock_guard<mutex> lock{ mut_ };
			ptr = inst_.load(memory_order_relaxed);
			if (ptr == nullptr) {
				ptr = new singleton();
				inst_.store(ptr, memory_order_release);
			}
		}
	
		return inst_;
	}
private:
	singleton(){};
	static mutex mut_;
	static atomic<singleton*> inst_;
};

mutex singleton::mut_;
atomic<singleton*> singleton::inst_;

来看一下汇编代码:

可以看到,编译器帮我们插入了必要的语句来保证指令的执行顺序。

使用现代C++中的call_once方法

call_once也是现代C++中引入的新特性,它可以保证某个函数只被执行一次。使用call_once的代码实现如下:

class singleton
{
public:
	static singleton* instance()
	{
		if (inst_ != nullptr) {
			call_once(flag_, create_instance);
		}
		return inst_;
	}
private:
	singleton(){}
	static void create_instance()
	{
		inst_ = new singleton();
	}
	static singleton* inst_;
	static once_flag flag_;
};

singleton* singleton::inst_ = nullptr;
once_flag singleton::flag_;

来看一下汇编代码:

可以看到,程序最终调用了__gthrw_pthread_once来保证函数只被执行一次。

使用静态局部变量

现在C++对变量的初始化顺序有如下规定:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

所以我们可以简单的使用一个静态局部变量来实现线程安全的单例模式:

class singleton
{
public:
	static singleton* instance()
	{
		static singleton inst_;
		return &inst_;
	}
private:
	singleton(){}
};

来看一下汇编代码:

可以看到,编译器已经自动帮我们插入了相关的代码,来保证静态局部变量初始化的多线程安全性。

全文完。

C++ 中实现线程安全单例模式,可以借助 C++11 引入的静态局部变量初始化线程安全特性。这种实现方式不仅简洁,而且避免了经典的双重检查锁定(Double-Checked Locking)问题。C++11 的内存模型保证了静态局部变量的初始化是线程安全的,因此可以在不使用锁的情况下实现线程安全单例模式 [^1]。 ### 使用静态局部变量实现线程安全单例 ```cpp class Singleton { public: static Singleton& getInstance() { static Singleton instance; return instance; } // 禁止复制和赋值 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; protected: Singleton() {} ~Singleton() {} }; ``` 上述实现中,`static Singleton instance;` 的初始化是线程安全的,C++11 标准保证了这一点。这种实现方式比传统的双重检查锁定更简洁,并且避免了潜在的内存模型问题 [^1]。 ### 使用双重检查锁定实现线程安全单例 如果需要在 C++11 之前的标准中实现线程安全单例模式,可以采用双重检查锁定(Double-Checked Locking Pattern,DCLP)。这种模式通过使用互斥锁和原子操作来确保单例对象的初始化是线程安全的。 ```cpp #include <mutex> class Singleton { public: static Singleton& getInstance() { Singleton* tmp = instance.load(std::memory_order_acquire); if (!tmp) { std::lock_guard<std::mutex> lock(mutex_); tmp = instance.load(std::memory_order_relaxed); if (!tmp) { tmp = new Singleton(); instance.store(tmp, std::memory_order_release); } } return *tmp; } // 禁止复制和赋值 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; protected: Singleton() {} ~Singleton() {} private: static std::atomic<Singleton*> instance; static std::mutex mutex_; }; std::atomic<Singleton*> Singleton::instance; std::mutex Singleton::mutex_; ``` 在上述实现中,`std::atomic` 和 `std::memory_order` 被用来确保内存访问顺序的正确性,而 `std::mutex` 则用于在初始化阶段保护资源的并发访问。这种实现方式依赖于线程局部存储(Thread Local Storage)来减少锁的使用,从而提高性能 [^3]。 ### 使用静态成员变量和销毁器实现单例 另一种实现方式是使用静态成员变量和一个销毁器类来确保单例对象的生命周期管理。这种方法适用于需要显式控制单例对象销毁的情况。 ```cpp template <typename T> class Destroyer { public: ~Destroyer() { if (T::_instance) { delete T::_instance; T::_instance = nullptr; } } void SetDoomed(T* instance) { T::_instance = instance; } }; class Singleton { public: static Singleton* Instance() { if (!_instance) { _instance = new Singleton(); _destroyer.SetDoomed(_instance); } return _instance; } friend class Destroyer<Singleton>; // 禁止复制和赋值 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; protected: Singleton() {} virtual ~Singleton() {} private: static Singleton* _instance; static Destroyer<Singleton> _destroyer; }; Singleton* Singleton::_instance = nullptr; Destroyer<Singleton> Singleton::_destroyer; ``` 在这个实现中,`Destroyer` 类负责在程序结束时释放单例对象的资源。这种方法确保了单例对象的析构函数在程序退出时被调用,从而避免资源泄漏 [^2]。 ### 总结 C++ 中实现线程安全单例模式有多种方式,其中 C++11 提供的静态局部变量初始化线程安全特性是最为简洁和推荐的方式。对于需要兼容旧版本 C++ 的项目,可以采用双重检查锁定或静态成员变量结合销毁器的方式实现线程安全单例模式
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值