C++并发与多线程(三)

单例设计模式共享数据分析、解决,call_once

什么是设计模式

设计模式是代码的特殊写法,程序灵活,维护起来可能方便,但是代码可读性低。

用设计模式理念写出的代码是很晦涩的,《head first》开始流行。是外国对于 特别大的项目,把项目开发经验、模块划分经验,总结整理成 设计模式(先有开发需求,后有理论总结和整理)。

设计模式有独特的有点,需要活学活用,不能生搬硬套。

单例设计模式

单列设计模式,使用的频率比较高;

单例:整个项目中,有某个或者某些特殊的类,属于该类的对象,我只能创建1个。

#include <iostream>
#include <windows.h>
using namespace std;

class MyCAS // 这是一个单例类
{
private:
	MyCAS() {}//私有化的构造函数 不能直接生成类对象

private:
	static MyCAS* m_instance;//静态成员变量

public:
	static MyCAS* GetInstance()
	{
		if (m_instance == NULL)
		{
			m_instance = new MyCAS();
			static CGarhuishou cl;
		}
		return m_instance;
	}
	class CGarhuishou //类中套一个类,用来释放对象
	{
	public:
		~CGarhuishou()
		{
			if (MyCAS::m_instance != NULL)
			{
				delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	};
	void func()
	{
		cout << "测试" << endl;
	}

};
//类静态变量初始化
MyCAS* MyCAS::m_instance = NULL;

int main()
{
	MyCAS* p_a = MyCAS::GetInstance(); //创建一个对象,返回该类对象的指针。
	//MyCAS* p_b = MyCAS::GetInstance(); //都指向同一个对象。
	p_a->func();
	MyCAS::GetInstance()->func();//与上一行相同
}

主要思想是将构造函数设为私有,然后通过静态成员函数给静态成员变量赋值为对象,这样可以通过判断静态成员变量来达到只能够有一个对象的目的。因为程序中new了一个对象,就用类中套类的方式,在另一个类析构的时候释放new的堆区。

整个类的写法就是一种设计模式。

单例设计模式共享数据问题分析、解决

问题:需要在自己创建的线程(而不是主线程中)创建MyCAS这个单列类对象,这种线程可能不止一个(最少两个)。这时,Get Instance()需要互斥。

直接加锁会导致其他线程需要调用构造函数时都等待,减少了效率。

#include <windows.h>
#include <iostream>
#include<mutex>

using namespace std;

std::mutex resource_mutex;

class MyCAS // 这是一个单例类
{
private:
	MyCAS() {}//私有化的构造函数 不能直接生成类对象

private:
	static MyCAS* m_instance;//静态成员变量

public:
	static MyCAS* GetInstance()
	{
		//提高效率
		//如果m_instance != NULL条件成立。则表示m_instance已经被new过了。
		//如果m_instance == NULL,不代表m_instance一定没有被new过。
		if (m_instance == NULL)//双重锁定
		{
			std::unique_lock<std::mutex> mymutex(resource_mutex);//自动加锁 效率太低
			if (m_instance == NULL)
			{
				m_instance = new MyCAS();
				static CGarhuishou cl;
			}
			return m_instance;
		}
	}
	class CGarhuishou //类中套一个类,用来释放对象
	{
	public:
		~CGarhuishou()
		{
			if (MyCAS::m_instance != NULL)
			{
				delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	};
	void func()
	{
		cout << "测试" << endl;
	}

};
//类静态变量初始化
MyCAS* MyCAS::m_instance = NULL;


void mythread()
{
	cout << "我的线程开始执行" << endl;
	MyCAS* p_a = MyCAS::GetInstance();

	cout << "我的线程执行结束" << endl;
	return;
}
int main()
{

	//如下两个线程,虽然两个线程是同一个入口函数,但是两个线程,所以这里会有两个通路同时开始执行mythread这个函数
	std::thread mytobj1(mythread);
	std::thread mytobj2(mythread);
	mytobj1.join();
	mytobj2.join();
	
}

双重锁的存在能够很好的解决问题。

std::call_once()

call_once()是一个函数模板,C++11引入的函数,该函数的第二个参数是一个函数名a();

功能是能够保证函数a()只被调用一次。

call_once()具备互斥量的能力、而且效率上比互斥量消耗资源更少。

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

修改类代码:

std::once_flag g_flag;//这时一个系统第一的标记class MyCAS // 这是一个单例类{	static void CreateInstance () //只被调用一次		{			m_instance = new MyCAS();			static CGarhuishou cl;		}private:	MyCAS() {}//私有化的构造函数 不能直接生成类对象private:	static MyCAS* m_instance;//静态成员变量public:	static MyCAS* GetInstance()	{		std::call_once(g_flag, CreateInstance);			return m_instance;	}	class CGarhuishou //类中套一个类,用来释放对象	{	public:		~CGarhuishou()		{			if (MyCAS::m_instance != NULL)			{				delete MyCAS::m_instance;				MyCAS::m_instance = NULL;			}		}	};	void func()	{		cout << "测试" << endl;	}};

condition_variable、wait、notify_one、notify_all

条件变量std::condition_variable、wait()、notify_one()

线程A:等待一个条件满足

线程B:专门往消息队列中传入数据

condition_variable:条件变量,实际上是一个和条件相关的类,说白了就是等待一个条件达成。这个类需要和互斥量来配合工作,用的时候我们要生成这个类的对象。

wait()是condition_variable的成员函数,用来等待一个东西,如果二个参数lambda表达式返回值是true,直接返回;如果第二个参数lambda表达式返回值是false,那么wait()将解锁互斥量并阻塞到本行,阻塞到其他某个线程调用notify_one() 成员函数为止。

如果wait()没有第二个参数,那么就跟第二个参数lambda表达式返回false效果一样。

notify_one()将阻塞状态的wait() 唤醒,wait恢复工作:a)wait() 不断尝试重新获取互斥量锁,如果获取不到,那么流程就开在wait等着获取。如果获取到了,上锁、继续往下面走,执行b。

​ b.1)如果wait有第二个参数(lambda),就判断这个lambda表达式,如果表达式为false,那么重复wait阻塞解锁的过程。

​ b.2)如果lambda表达式为true,则wait返回。代码继续往下走(此时互斥锁被锁住)。

​ b.3)如果wait没有第二个参数,则wait返回,代码继续往下走。

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
using namespace std;


class A
{
public:
	//把收到的消息放入队列的线程
	void inMsgRecvQueue()
	{
		
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
			std::unique_lock<std::mutex> sbguard(my_mutex1);
			msgRecvQueue.push_back(i);//假设数字i就是收到的命令,直接加入消息队列
			my_cond.notify_one();//尝试把wait线程唤醒。
		}

	}

	void outMsgRecvQueue()
	{

		int command = 0;
		while (true)
		{
			std::unique_lock<std::mutex> sbguard1(my_mutex1);
			my_cond.wait(sbguard1,[this] {
				if (!msgRecvQueue.empty())
					return true;
				return false;
				});
			command = msgRecvQueue.front();
			msgRecvQueue.pop_front();
			sbguard1.unlock();
			cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
		}

	}

private:

	list<int> msgRecvQueue;//容器,专门用于代表玩家发过来的命令
	std::mutex my_mutex1;//一个互斥量
	std::mutex my_mutex2;//另一个互斥量
	std::condition_variable my_cond;//生成一个条件变量对象
};



int main()
{
	A myobja;

	thread myOutnMsgobj(&A::outMsgRecvQueue, &myobja);//第二个参数是引用,才能保证线程使用同一个对象。
	thread myInMsgobj(&A::inMsgRecvQueue, &myobja);
	myOutnMsgobj.join();
	myInMsgobj.join();


	return 0;
}

上面代码有一些问题:1.wait拿锁的同时,输入线程也在不间断的加锁,可能输入十几个最后只输出了一个。

​ 2.假如唤醒的线程正在处理一个事务,需要一段时间,而不是正卡在wait()那里等待,那么此时这次的notify_one()就没有效果。

上述代码深入思考

稳定才是最重要的,不一定非要用这种方法,因为上述问题目前也还没有解决。

notify_all()

notify_one()只能唤醒一个线程;

notify_all()可以通知唤醒所有线程;

async、future、packaged_task、promise

std::async、std::future创建后台任务并返回值

如果希望线程执行完毕后返回结果,std::async、std::future可以很好的得到效果。

std::async 是函数模板,用来启动一个异步任务,启动一个异步任务之后,它返回一个std::future,std::future是一个类模板

异步任务:就是自动创建一个线程,并开始执行对应的线程入口函数,它返回一个std::future对象,对象里面就含有线程入口函数所返回的结果(线程返回的结果)。可以通过调用fuiture对象的成员函数 get() 成员函数来得到结果。

”future“ 提供一种访问异步操作结果的机制,结果可能不能马上拿到,但是在线程执行完毕的时候就能够得到结果。future对象会保存一个值,线程执行完毕后能够拿到。

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
#include<future>
using namespace std;

int mythread()//线程入口函数
{
	cout << "mythread() start" << "thread id = " << std::this_thread::get_id() << endl;
	std::chrono::seconds dura(5);//休息5s
	std::this_thread::sleep_for(dura);
	cout << "mythread() end" << "thread id = " << std::this_thread::get_id() << endl;
	return 5;
}



int main()
{

	cout << "main" << " thread id = " << std::this_thread::get_id() << endl;

	std::future<int> result = std::async(mythread);//创建一个线程开始执行,绑定关系
	cout << "continue.....!" << endl;
	int def;
	def = 0;
	cout << result.get() << endl;//阻塞在这里等待线程执行完毕拿到结果。
	cout << "主线程执行完毕!" << endl;

	return 0;
}

运行结果:

main thread id = 10180
continue.....!
mythread() startthread id = 6716

mythread() endthread id = 6716
5
主线程执行完毕!

总结:以上程序是通过get()函数等待线程结束并且返回结果。get() 函数在没有拿到线程返回值的时候就会一直阻塞等待,所以在使用future的时候必须保证线程有返回值。且get() 只能调用一次。

为什么get()只能调用一次?

get()函数设计是一个移动语义,调用get时在移动函数中完成运算,并且里面对象会移动变为空。

result.wait();函数可以等待线程结束,但是不获取返回值。

ps:和之前一样,可以用类的成员函数来做线程入口函数。

改写之后:

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
#include<future>
using namespace std;

class A {
public:
	int mythread(int mypar)//线程入口函数
	{
		cout << mypar << endl;
		cout << "mythread() start" << "thread id = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000);//休息5s
		std::this_thread::sleep_for(dura);
		cout << "mythread() end" << "thread id = " << std::this_thread::get_id() << endl;
		return 5;
	}
};



int main()
{
	A a;
	int tmpper = 12;
	cout << "main" << " thread id = " << std::this_thread::get_id() << endl;
	std::future<int> result = std::async(&A::mythread,&a,tmpper);//第二个参数是一个对象引用,第三个参数是线程需要的传参。
	cout << "continue.....!" << endl;
	int def;
	def = 0;
	cout << result.get() << endl;//阻塞在这里等待线程执行完毕拿到结果。
	cout << "主线程执行完毕!" << endl;

	return 0;
}

可以通过向std::async() 传递一个参数,该参数是std::lunch类型(枚举)类型,来达到特殊的目的;

a)std:launch::diferred : 表示线程入口函数调用被延迟到std::future的wait() 或者get() 函数调用时才执行; 如果wait()或者get() 没有被调用,那么线程会执行吗?

std::future<int> result = std::async(std::launch::deferred,&A::mythread,&a,tmpper); 注释掉get()后,可以发现线程没有执行。实际上,线程没有被创建。这时,只有在 get() 或者 wait() 的时候才执行

main thread id = 13292
continue.....!
12
mythread() startthread id = 13292
mythread() endthread id = 13292
5
主线程执行完毕!

但是这时是在主线程中执行的。所以std::launch::deferred不仅是延迟调用,并且没有创建新线程,是在主线程中调用的。

b)std::launch::async:表示在调用async函数的时候就开始创建线程;

修改std::future<int> result = std::async(std::launch::async,&A::mythread,&a,tmpper);

main thread id = 10724
continue.....!
12
mythread() startthread id = 14468
mythread() endthread id = 14468
5
主线程执行完毕!

可以看见,子线程被创建,调试时可以发现,在std::future<int> result = std::async(std::launch::async,&A::mythread,&a,tmpper);执行的时候就会创建线程。

async()函数默认用的就是std::launch::async。

std::packaged_task

packaged_task是一个类模板,模板参数是各种可调用对象;通过packaged_task 可以将各种可调用对象包装起来,方便将来作为线程入口函数将来调用。

#include<thread>
#include <iostream>
#include <windows.h>
#include<mutex>
#include<list>
#include<map>
#include<vector>
#include<mutex>
#include<future>
using namespace std;

int mythread(int mypar)//线程入口函数
{
	cout << mypar << endl;
	cout << "mythread() start" << "thread id = " << std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);//休息5s
	std::this_thread::sleep_for(dura);
	cout << "mythread() end" << "thread id = " << std::this_thread::get_id() << endl;
	return 5;
}

int main()
{

	//A a;
	//int tmpper = 12;
	cout << "main" << " thread id = " << std::this_thread::get_id() << endl;

	std::packaged_task<int(int)> mypt(mythread);//我们把函数mythread通过packaged_task包装起来
	std::thread t1(std::ref(mypt), 1);//线程直接开始执行,第二个参数作为线程入口函数的参数
	t1.join();
	std::future<int> result = mypt.get_future();//std::future对象里包含有线程入口函数的返回结果
	cout << result.get() << endl;

	cout << "主线程执行完毕!" << endl;
	 
	return 0;
}

主要是为了 std::future<int> result = mypt.get_future();这一步。

由于packaged_task 可以包装各种可调用对象,那么也可以调用lambda表达式:

int main()
{

	//A a;
	//int tmpper = 12;
	cout << "main" << " thread id = " << std::this_thread::get_id() << endl;

	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		cout << "mythread() start" << "thread id = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000);//休息5s
		std::this_thread::sleep_for(dura);
		cout << "mythread() end" << "thread id = " << std::this_thread::get_id() << endl;
		return 5;
		});//我们把函数mythread通过packaged_task包装起来
	std::thread t1(std::ref(mypt), 1);//线程直接开始执行,第二个参数作为线程入口函数的参数
	t1.join();
	std::future<int> result = mypt.get_future();//std::future对象里包含有线程入口函数的返回结果
	cout << result.get() << endl;

	cout << "主线程执行完毕!" << endl;
	 
	return 0;
}

运行结果和之前一样。

打包好的对象也是一个可调用对象,可以直接调用:

int main()
{

	//A a;
	//int tmpper = 12;
	cout << "main" << " thread id = " << std::this_thread::get_id() << endl;

	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		cout << "mythread() start" << "thread id = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000);//休息5s
		std::this_thread::sleep_for(dura);
		cout << "mythread() end" << "thread id = " << std::this_thread::get_id() << endl;
		return 5;
		});//我们把函数mythread通过packaged_task包装起来
	mypt(105);//直接调用
	std::future<int> result = mypt.get_future();//std::future对象里包含有线程入口函数的返回结果
	cout << result.get() << endl;

	cout << "主线程执行完毕!" << endl;
	 
	return 0;
}

执行结果:

main thread id = 3232
105
mythread() startthread id = 3232
mythread() endthread id = 3232
5
主线程执行完毕!

此时就相当于一个函数调用,在主线程中进行。

还可以用迭代器:

vector <std::packaged_task<int(int)>> mytasks;
int main()
{

	cout << "main" << " thread id = " << std::this_thread::get_id() << endl;

	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		cout << "mythread() start" << "thread id = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000);//休息5s
		std::this_thread::sleep_for(dura);
		cout << "mythread() end" << "thread id = " << std::this_thread::get_id() << endl;
		return 5;
		});//我们把函数mythread通过packaged_task包装起来
	mytasks.push_back(std::move(mypt));//这里用的是move放入容器,防止拷贝进容器
	std::packaged_task<int(int)> mypt2;
	auto iter = mytasks.begin();
	mypt2 = std::move(*iter);
	mytasks.erase(iter);
	mypt2(105);


	std::future<int> result = mypt2.get_future();//std::future对象里包含有线程入口函数的返回结果
	cout << result.get() << endl;

	cout << "主线程执行完毕!" << endl;
	 
	return 0;
}

效果一样。

std::promise

std::promise也是一个类模板,能够在某个线程中给它赋值,然后在其他线程中把值取出来用。

void mythread(std::promise<int>& tmpp, int calc)//注意第一个参数
{
	//做一系列的复杂操作
	calc++;
	calc *= 10;
	//....
	std::chrono::milliseconds dura(5000);//休息5s
	std::this_thread::sleep_for(dura);
	int result = calc;
	tmpp.set_value(result);//保存结果到tmpp这个对象中。

}
void mythread2(std::future<int>& tmpf)
{
	auto result = tmpf.get();
	cout << "mythread2 result = " << result << endl;
	return;
}
int main()
{

	cout << "main" << " thread id = " << std::this_thread::get_id() << endl;
	std::promise<int> myprom;//申明一个promise对象,保存的值为int
	std::thread t1(mythread, std::ref(myprom), 180);
	t1.join();

	//获取结果值
	std::future<int> ful = myprom.get_future();
	std::thread t2(mythread2, std::ref(ful));
	t2.join();
	cout << "主线程执行完毕!" << endl;
	return 0;
}

运行结果:

main thread id = 6472
mythread2 result = 1810
主线程执行完毕!

可以看出实现了线程之间参数的传递。

小结

这么多线程函数,到底怎么用,什么时候用?

学习这些东西的目的并不是都要用在实际开发中,如果能够用最少的东西写出一个稳定、高效的多线程程序,更好。

学习过程中,必须学习别人写的好的代码,实现自己的积累,技术就会大幅度提升。学习这么多能够很好的读高手的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

word_no_bug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值