IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 单例模式的线程安全 -> 正文阅读

[C++知识库]单例模式的线程安全

前言

单例模式(Singleton)几乎是人尽皆知的设计模式了,它限制一个对象只能实例化一次,且该对象的生命周期一般与整个应用的生命周期一致(否则,单例模式完全可以被普通类对象替代)。单例对象应该允许多线程访问,确保单例对象是线程安全的十分有必要。

单例由于其生命周期特点,一般被实现为指针对象或静态对象,下面将分别讨论这两种情况的线程安全实现。

返回静态指针

下面实现一个基本的单例对象:

// Example 1
class Singleton
{
private:
    Singleton() = default;         // 不允许用户实例化对象
    static Singleton* singleton_;
public:
    Singleton(Singleton& other) = delete;        // 不允许拷贝初始化
    void operator=(const Singleton&) = delete;  // 不允许拷贝赋值

    static Singleton* Instance() {
    	if (singleton_ == nullptr) {
    		singleton_ = new Singleton();
    	}
    	return singleton_;
    }
};

Singleton* Singleton::singleton_= nullptr;;

上面的例子中,Singleton 类的构造函数被隐藏,不允许用户直接实例化,同时禁止对象的拷贝。注意该 Singleton 有个类型为 Singleton 的静态成员指针变量,指向在静态成员函数 Instance() 中被实例化的对象,该对象只能被实例化一次,并在创建对象后将指针返回。这就实现了一个看起来还不错的单例对象。

但是,我们只看到了 new 操作符,而没有 delete,上述代码将造成内存泄露。让我们增加下面的代码。

// Example 2
class Singleton
{
// same with Example 1
private:
	~Singleton(){
		if (singleton_) {
	 		delete singleton_;
		}
	}
}

是不是这样就可以了呢?不,这不仅没有解决问题,反而引入了更严重的漏洞。
首先 singleton_ 指针是 static 对象,对象被析构和内存被释放的时机是不确定的;其次我们在析构函数中 delete singleton_,而该操作会再次调用析构函数,淦,递归死循环,这真是灾难。

正确的做法是,将 delete 操作放到静态成员函数中,由用户在合适的时机释放资源(用户有责任这么做),例如在退出 main() 函数前调用 Singleton::DestoryInstance(),实现如下(Example 3):

// Example 3
class Singleton
{
// same with Example 1
public:
	static DestoryInstance() {
    	if (singleton_) {
    		delete singleton_;
    		singleton = nullptr;  // 使 Instance() 再次变得可用,如果有需要!
    	}
    } 
}

目前为止,这个单例类我们实现的还算完整,它在单线程应用程序可以安全工作,但多线程环境中仍有风险。

考虑到 singleton_ 在申请资源(new 操作)完成之前,多个线程可能已经完成了 singleton_ == nullptr 的判断条件,此时这些线程都将执行 new 操作,这就创建了多个 Singleton 实例,导致单例模式设计失败。

解决方案就是引入同步化技术,也是将程序块 if (singleton_ == nullptr) { singleton_= new Singleton() } 变成同步(原子)操作,一次值允许一个线程执行。实现如下(Example 4):
值得一提的事,在加锁之前就进行了一次条件判断(singleton_ == nullptr),这是为了在多线程中,避免不必要的加锁等待,提高程序运行效率。

// Example 4
class Singleton
{
private:
    Singleton() = default;         // 不允许用户实例化对象
    static Singleton* singleton_;
    static std::mutex mtx;
public:
    Singleton(Singleton& other) = delete;        // 不允许拷贝初始化
    void operator=(const Singleton&) = delete;  // 不允许拷贝赋值

    static Singleton* Instance() {
    	if (singleton_ == nullptr) {             // 提高效率,避免不必要的加锁
    		std::lock_guard<std::mutex> lock(mtx);	// 同步以下代码
	    	if (singleton_ == nullptr) {
	    		singleton_ = new Singleton();
	    	}
    	}
    	return singleton_;
    }
    
	static DestoryInstance() {
    	if (singleton_) {
    		delete singleton_;
    		singleton = nullptr;  // 使 Instance() 再次变得可用,如果有需要!
    	}
    } 
};

Singleton* Singleton::singleton_= nullptr;;

返回静态引用

将单例对象声明为静态存储区变量的实现如下:

class Singleton
{
private:
    Singleton() = default;         // 不允许用户实例化对象
public:
    Singleton(Singleton& other) = delete;        // 不允许拷贝初始化
    void operator=(const Singleton&) = delete;  // 不允许拷贝赋值

    static Singleton& Instance() {
    	static Singleton singleton_
    	return singleton_;
    }
};

以上代码看起来过于简单,而且非线程安全。实则不然,在 C++11 之后,静态存储期变量的初始化细节有所变化,即使在多线程环境中,多线程同时初始化,也能保证静态变量只会初始化一次。编译器已经为我们做了上述中双重检查锁的工作。

比较以上两种实现,Instance() 返回指针的方式的唯一优点是用户可以在程序退出之前多次销毁和重建单例对象,当然这和单例模式的概念冲突。在 Modern C++ 编程中,返回局部静态对象的引用,是更加简洁高效的手段。

单例模板

当一个程序有多个单例对象需要实现时,构造一个单例模板类,让其他单例类继承该模板基类,能节省不少时间。

// 单例模板基类
template <class T>
class SingletonBase
{
protected:                       // protected: 让派生类可以访问
	SingletonBase() {}
public:
	SingletonBase(SingletonBase const &) = delete;
	SingletonBase& operator=(SingletonBase const&) = delete;
	static T& instance()
	{
		static T single;
		return single;
	}
};

一个简单的实例:

class ConsoleLogger : public SingletonBase<ConsoleLogger> 
{
private:
	Single() {}
	friend class SingletonBase<ConsoleLogger>;
public:
	void Debug() { std::cout << "debug" << std::endl; }
};

由于基类需要访问派生类的私有默认构造函数,所以基类是派生类的一个友元类。

C/C++Linux服务器开发/后台架构师【零声教育】 :
https://ke.qq.com/course/417774?flowToken=1041371

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-01-01 13:42:06  更:2022-01-01 13:42:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 11:11:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码