从C++单例模式到线程安全详解

所属分类: 软件编程 / C 语言 阅读数: 73
收藏 0 赞 0 分享

先看一个最简单的教科书式单例模式:

class CSingleton
{
public:
	static CSingleton* getInstance()
	{
		if (NULL == ps)
		{//tag1
			ps = new CSingleton;
		}
		return ps;
	}

private:
	CSingleton(){}
	CSingleton & operator=(const CSingleton &s);
	static CSingleton* ps;
};

CSingleton* CSingleton::ps = NULL;

有2个要点:

1.private的构造函数和=操作符,用于防止类外的实例化和被复制;

2.static的类指针和get方法。

在大多数单线程情况下,以上代码大都会运行得很好,除非遇到中断:

1.当程序运行到tag1 处触发了中断;
2.中断处理程序恰调用的也是getInstance函数。

可想而知,这和多线程的情况类似,假设线程A 运行到tag1处,还没来得及new,此时ps仍然是NULL,线程B(或中断处理程序) 同时也运行到此通过if判断,那么将会实例化2个CSingleton对象,显然是不对的。

为了解决上述问题,自然而然,最容易想到也最常用的方法是加锁,因此getInstance改成这样:

	static CSingleton* getInstance()
	{
		lock();//伪代码
		if (NULL == ps)
		{
			ps = new CSingleton;
		}
		return ps;
	}

加了锁以后貌似解决了上述问题,但也同样带来了新的问题:如果程序到处是诸如:

CSingleton::instance()->aaaa();
CSingleton::instance()->bbbb();
CSingleton::instance()->cccc();

这样的调用,除了第一次的lock()有用外,后面的都是在做无用功,lock()的代价说大不大,但在某些情况下还是会提高程序延迟,这对追求完美的程序猿来说是完全无法接受的。

于是乎,咱想出了一个办法:

	static CSingleton* getInstance()
	{
		if (NULL == ps)//这里加了次判断,只有第一次才会为true而调用lock()
		{
			lock();//伪代码
			if (NULL == ps)
			{
				ps = new CSingleton;
			}
		}
		return ps;
	}

很久以后我才知道,这个方法有个很高大上的名字,叫做双重检查锁定模式,简称DCLP(Double Checked Locking Pattern)。

DCLP很好地解决了多次调用不必要的lock()。

然而,你们以为这样就完了?too young。。

DCLP在多线程下仍然存在2个根本问题:

1.程序的指令执行顺序不确定;
2.编译器优化问题。

先说2,在某些编译器下,以上的两个if判断只会执行一个,甚至一个都不执行,原因是编译器认为至少有一个if判断是多余的,它自动帮助我们优化了代码。

再说1,ps = new CSingleton; 这条语句会被拆分为这样的三个步骤执行:

1.为要new的对象开辟一块内存;
2.构造该对象,填入这块内存;
3.将ps指针指向这块内存。

以上三个步骤,2和3的顺序是不确定的,可能先2后3,也可能先3后2。。。

实际执行时可能是这样的:

	static CSingleton* getInstance()
	{
		if (NULL == ps)
		{
			lock();//伪代码
			if (NULL == ps)
			{    //伪代码
				ps = xx;//step 3
				new sizeof(CSingleton);//step 1
				new CSingleton;//step 2
			}
		}
		return ps;
	}

如果编译器按上述顺序执行代码,考虑如下状况:

线程A 执行到step 1还未执行后面的step 2,此时ps非空,但其指向的内存里面的内容还未被构造出来,于此同时线程B 进入这个函数,判断ps非空直接返回ps,但是调用者此时访问的ps内存实际内容CSingleton还没被构造呢,这是一块地址正确大小正确但内部数据不明的东西,当然会出错(调用者一般这么调用:CSingleton::getInstance()->aa();  CSingleton::getInstance()->bb();  CSingleton::getInstance()->cc();........此时的aa,bb,cc是啥玩意儿?)。

这也是为什么加上volatile关键字仍然不可以解决同步问题,volatile只解决了编译器优化问题,却无法控制机器指令执行顺序。

很遗憾的是,C/C++本身在设计时是不考虑多线程问题的,也就是说,要处理多线程问题还要程序猿自己想办法填坑。。

说了这么多,我们要讨论的问题仍然没有解决,庆幸的是,C++ 11提供了内存栅栏技术来解决这个问题,这里不赘述,有兴趣的读者可以自己搜索资料看看,不过是一些api调罢了。

那么,C++ 11 以前的代码如何解决这个问题呢?很不幸,并没有很好的解决方案,一种可行的方案是,程序中不要到处这么调用这个单例对象:

CSingleton::getInstance()->aa(); 
CSingleton::getInstance()->bb();
CSingleton::getInstance()->cc();

而是在程序开始就初始化缓存这个单例对象:

CSingleton* const g_ps = CSingleton::getInstance();//程序一开始就缓存这个单例对象
g_ps->aa();
g_ps->bb();
g_ps->cc();

但是如此带来的问题是程序一开始就实例化了这个单例对象,对象在整个程序的声明周期存在,这貌似叫饿汉式,而之前那种叫懒汉式,孰轻孰重,只有根据实际情况取舍了。

以上就是小编为大家带来的从C++单例模式到线程安全详解全部内容了,希望大家多多支持脚本之家~

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多