C++ 并发与多线程学习笔记(五)单例设计模式 共享数据分析

在开始学习之前,先简单讲讲对于设计模式的概念,设计模式是一些代码的写法,并非什么新奇的技术,是一些歪果仁的模块化编程的经验总结,是一种设计程序的思想或者说思路模板。在前几年设计模式刚被引入的时候,大家觉得很高大上,因此曾被部分夸大,但实际中并不是必要的,用设计模式写出来的代码比较晦涩,维护、接手的时候都有一定难度。设计模式有它的优点,要活学活用,不要深陷其中,正确的认识它。所有实用的编程方法和技术,是从需求出发的。

单例设计模式

需求:整个项目中,有某个或者某些特殊的类,属于该类的对象,只能创建一个。整个项目内都不能被第二次生成。
应用实例:

  1. 一个班级只有一个班主任。
  2. Windows是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  3. 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
    优点:
  4. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
  5. 避免对资源的多重占用(比如写文件操作)。

缺点:不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

单例模式是使用频率比较高的一种设计模式。常用单例类,网上java的比较多,这里给出C++的代码。

#include <iostream>


class SingleClass
{
private:
	//私有化构造函数 使其无法被外部创建
	SingleClass() {} 
private:
	//私有静态成员指针,这里存储未来的唯一对象
	static SingleClass* m_instance;
public:
	//获取该对象的方法
	static SingleClass *GetInstance()
	{
		//仅创建一次的对象
		if (m_instance == NULL)
		{
			//对象被实例化
			m_instance = new SingleClass();
		}
		return m_instance;
	}
	void testFunc()
	{
		std::cout << "test" << std::endl;
	}
};

SingleClass *SingleClass::m_instance = NULL;

int main()
{
	using namespace std;
	//获取该类对象的指针
	SingleClass *p_a = SingleClass::GetInstance();//创建这个对象
	SingleClass *p_b = SingleClass::GetInstance();
	cout << p_a << endl;
	cout << p_b << endl;
    return 0;
}

输出结果:
在这里插入图片描述
可以看到两个不同的指针,经过两次调用Get::Instance后指向的是相同的变量,此后在整个项目中,不论怎么调用SingleClass的GetInstance,返回都是唯一的这个对象,永远不可能有第二个。

有经验的读者可能会想到一个问题,这里单例类使用了静态成员指针,当需要的时候,我们怎样才能正确的释放它呢?使用智能指针来包装这个指针是一种办法。这里介绍另一种方法,清理类。

直接释放静态成员指针会导致内存泄漏,在析构函数中释放也会异常,本质上是操作系统替我们回收了

在SingleClass中写一个类,这个类只有一个作用,就是释放SingleClass的指针。在创建SingleClass对象的时候同时实例化一个静态清理类,修改代码如下:

#include <iostream>

class SingleClass
{
private:
	//私有化构造函数 使其无法被外部创建
	SingleClass() {} 
private:
	//私有静态成员指针,这里存储未来的唯一对象
	static SingleClass* m_instance;
public:
	//获取该对象的方法
	static SingleClass *GetInstance()
	{
		//仅创建一次的对象
		if (m_instance == NULL)
		{
			//对象被实例化
			m_instance = new SingleClass();
			//实例化清理类
			static Release_this rt; 
		}
		return m_instance;
	}
	//清理类
	class Release_this
	{
	public:
		~Release_this()
		{
			if (SingleClass::m_instance)
			{
				delete SingleClass::m_instance;
				SingleClass::m_instance = NULL;
			}
		}
	};
	void testFunc()
	{
		std::cout << "test" << std::endl;
	}
};

SingleClass *SingleClass::m_instance = NULL;

int main()
{
	using namespace std;
	//获取该类对象的指针
	SingleClass *p_a = SingleClass::GetInstance();//创建这个对象
	SingleClass *p_b = SingleClass::GetInstance();
	cout << p_a << endl;
	cout << p_b << endl;
	//使用这个对象,初始化我们需要的数据
	p_a->testFunc();
	SingleClass::GetInstance()->testFunc();
	//这两种方式是一样的
    return 0;
}

这样就能在这个清理类析构的时候同时释放掉静态指针。

单例设计模式共享数据问题

多线程编程中面临的问题:需要我们在自己创建的线程(不是主线程)中来实例化单例对象,这种线程可能不止一个(最少2个),这个时候GetInstance需要互斥使用,我们将GetInstance的代码修改如下:

static SingleClass *GetInstance()
{	
	std::unique_lock<std::mutex> my_unique(my_mutex);
	if (m_instance == NULL)
	{
		m_instance = new SingleClass();
		static Release_this rt;
	}
	return m_instance;
}

这个时候就要被老板同事吐槽了,就这?每次GetInstance都要上道锁,不卡死?
那再改:

static SingleClass *GetInstance()
{	
	if (m_instance == NULL)
	{
		std::unique_lock<std::mutex> my_unique(my_mutex);
		m_instance = new SingleClass();
		static Release_this rt;
	}
	return m_instance;
}

老板:你明天不用来了

这样写有一个隐患,在多线程环境下,第一个线程已经进入if判断了,第二个线程也进入if判断了,那这个锁其实已经失效了,因为第一个new之后第二个还是会new,这里就需要使用双重锁定(检查):

static SingleClass *GetInstance()
{	

	if (m_instance == NULL)//双重锁定
	{
		std::unique_lock<std::mutex> my_unique(my_mutex);
		if (m_instance == NULL)
		{
			m_instance = new SingleClass();
			static Release_this rt;
		}
	}	
	return m_instance;
}

双重检查有一个细节,就是
if (m_instance != NULL)条件成立,则肯定表示m_instance已经被new过了
if (m_instance == NULL)条件成立,不代表m_instance一定没被new过
,只有m_instance在加完锁的条件下不为空,才能证明它确实不空。

std::call_once()

c++11引入的函数,该函数第二个参数是一个函数名a();
功能:保证函数a只能被调用一次。call_once具备互斥的能力,而且高效。

call_once需要一个标记结合使用,这个标记数std::once_flag,call_once就是通过这个标记来决定对应的函数a()是否执行,调用call_once成功后,call_once就把这个标记设置为一种已调用的,后续再次调用call_once,只要once_flag被设置为“已调用”,那么a()就不会再执行了。

现在我们利用call_once来改造单例类。


#include <iostream>
#include <mutex>
#include <thread>

std::once_flag g_flag;
class SingleClass
{
	//使用一个只会被调用一次的函数来创建单例对象
	static void CreateInstace()
	{
		m_instance = new SingleClass();
		static Release_this rt;
	}
private:
	//私有化构造函数
	SingleClass() {} 
private:
	static SingleClass* m_instance;
public:
	static SingleClass *GetInstance()
	{	
		std::call_once(g_flag, CreateInstace);
		return m_instance;
	}
	class Release_this
	{
	public:
		~Release_this()
		{
			if (SingleClass::m_instance)
			{
				delete SingleClass::m_instance;
				SingleClass::m_instance = NULL;
			}
		}
	};
	//输出测试
	void testFunc()
	{
		std::cout << SingleClass::GetInstance() << "地址来源于线程:" << std::this_thread::get_id() << std::endl;
	}

};

//线程入口函数
void threadFunc()
{
	std::cout <<"线程开始执行 id:" << std::this_thread::get_id() << std::endl;
	SingleClass* p_a = SingleClass::GetInstance();
	p_a->testFunc();
}

//初始化指针
SingleClass *SingleClass::m_instance = NULL;

int main()
{
	using namespace std;
	//获取该类对象的指针
	thread my_thread1(threadFunc);
	thread my_thread2(threadFunc);
	my_thread1.join();
	my_thread2.join();
    return 0;
}

两个线程会同时去调用创建对象的函数,但执行到call_once,后一个进入的线程就会被阻塞
运行结果:
在这里插入图片描述

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页