C++并发与多线程(二) 创建多个线程、数据共享问题分析、案例代码

  本文系列大部分来自c++11并发与多线程视频课程的学习笔记,系列文章有(不定期更新维护):

创建和等待多个线程

  如果想要创建多个子线程的话,我们可以把创建好的子线程入vector中来管理多个子线程。用vector<thread> thread_vec;创建一个数组,然后将多个子线程push进这个数组中并进行管理。

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

void Print(int num) {
	cout << "sub thread is is: " << std::this_thread::get_id() << " num is: " << num << endl;
	return;
}

int main() {
	vector<thread> thread_vec;

	for (int i = 0; i < 10; ++i) {
		thread_vec.push_back(thread(Print, i)); // 创建临时线程对象,并将其加入到thread_vec中去。
	}

	for (auto iter = thread_vec.begin(); iter != thread_vec.end(); ++iter) {
		iter->join(); // 等待所有子线程返回
	}

	cout << "main thread done and pid is: " << std::this_thread::get_id() << endl;
}

  上述代码的执行结果如下所示:

  可以看到上述代码的子线程执行顺序是乱的,这个和操作系统内部对线程的调度机制有关,并且有些子线程没有运行完就被抢走了时间片。

数据共享问题分析

  在实际的应用场景中,我们可能会需要多个子线程对同一块数据进行读取或者写入操作。

只读的数据

  通过static vector<int> data = { 0, 1, 2 };设置一个全局变量,每个子线程都对这块数据进行读取处理。

#include <iostream>
#include <thread>
#include <vector>
using namespace std;

void Print(int num) {
	static vector<int> data = { 0, 1, 2 };
	cout << "sub thread is is: " << std::this_thread::get_id() << " num is: " << num << " address of data is : " << &data << endl;
	return;
}

int main() {

	vector<thread> thread_vec;

	for (int i = 0; i < 10; ++i) {
		thread_vec.push_back(thread(Print, i)); // 创建临时线程对象,并将其加入到thread_vec中去。
	}

	for (auto iter = thread_vec.begin(); iter != thread_vec.end(); ++iter) {
		iter->join(); // 等待所有子线程返回
	}

	cout << "main thread done and pid is: " << std::this_thread::get_id() << endl;
}

  上述代码的输出结果为:

有读有写

  比如说两个线程往里面写,八个线程读的话,如何来做呢?其实我们就是要满足读的时候不能写,写的时候不能读这样一种功能。我们可以做一个共享数据的保护案例代码:假设做一个网络游戏服务器,有两个线程,一个线程收集玩家命令,并把命令数据写到一个队列中。另外一个线程从队列中取出命令并执行。因为是对同一个队列进行操作,所以我们需要做一些上锁的操作。在开始之前,我们需要先理解一些基本的概念:

  1. 互斥量

  互斥量(mutex)的基本概念:互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()来给成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。互斥量使用要小心,尽量使得需要保护的数据不多也不少,少了达不到效果,多了影响效率。互斥量的用法:包含#include <mutex>头文件。std::mutex mymutex;就能创建一个mutex的成员变量。lock()unlock()mutex的两个成员函数。一般的使用用方法可以分为三步:1. lock(),2. 操作共享数据,3. unlock()。并且这里一定要注意:lock()unlock()要成对使用。

  C++中为了防止用户忘记unlock(),引入了一个叫做std::lock_guard的类模板。当用户忘记unlock()的时候不要紧,它会替你unlock()。用了std::lock_guard的类模板之后就不能使用lock()unlock()了。lock_guard sbguard(myMutex);取代lock()unlock(),用法如下:lock_guard<mutex> sbguard(myMutex1);lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()

  1. 死锁:死锁至少有两个互斥量mutex1mutex2

  线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。之后线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1。此时,死锁产生了,A锁着mutex1,需要锁mutex2B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。死锁的一般解决方案:只要保证多个互斥量上锁的顺序一样就不会造成死锁。

  std::lock()函数模板是C++11中引入的函数模板。std::lock(mutex1,mutex2……);。一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)。

  如果用了std::lock()函数模板但是怕忘记unlock()的话,我们可以采用std::lock_guardstd::adopt_lock参数。std::lock_guard std::mutex my_guard(my_mutex, std::adopt_lock);在退出的时候会调用析构函数,自动unlock()。加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock(),但是析构的时候会unlock()

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class A {
public:
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				//lock_guard<mutex> sbguard(myMutex1);
				lock(myMutex1, myMutex2); // 只有等所有互斥量都锁住才能锁成功。
				//myMutex2.lock(); // 先锁2再锁1,就会产生死锁。
				//myMutex1.lock();
				msgRecvQueue.push_back(i);
				myMutex1.unlock(); // 解锁的时候先解锁哪一个就无所谓。
				myMutex2.unlock();
			}
		}
	}
	bool outMsgLULProc(){
		myMutex1.lock(); // 这里与之前的先锁2后锁1会产生死锁。
		myMutex2.lock();
		if (!msgRecvQueue.empty())
		{
			cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
			msgRecvQueue.pop_front();
			myMutex2.unlock();
			myMutex1.unlock();
			return true;
		}
		myMutex2.unlock();
		myMutex1.unlock();
		return false;
	}

	void outMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			if (outMsgLULProc()){
				cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
			}
			else{
				// 消息队列为空
				cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
			}
		}
	}
private:
	list<int> msgRecvQueue;
	mutex myMutex1;
	mutex myMutex2;
};

int main()
{
	A myobja;
	mutex myMutex;
	thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
	thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
	myOutMsgObj.join();
	myInMsgObj.join();
	return 0;
}

  私有成员list<int> msgRecvQueue;为读取和写入的对象。读取的时候不能写入,写入的时候不能读取。用锁mutex myMutex1;mutex myMutex2;来解决对list成员的冲突读写问题。

unique_lock

  unique_lock是个类模板,在工作中,一般使用lock_guard就足够了。lock_guard取代mutexlock()unlock()unique_locklock_guard灵活很多(多出来很多用法),效率差一点,内存占用上可能会多一点。按照常规使用的话,lock_guardunique_lock没有什么区别。

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class A {
public:
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				std::unique_lock<std::mutex> sbguard1(myMutex1);
				msgRecvQueue.push_back(i);
			}
		}
	}
	bool outMsgLULProc(){
		std::unique_lock<std::mutex> sbguard1(myMutex1);
		if (!msgRecvQueue.empty())
		{
			cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
			msgRecvQueue.pop_front();
			return true;
		}
		return false;
	}

	void outMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			if (outMsgLULProc()){
				cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
			}
			else{
				// 消息队列为空
				cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
			}
		}
	}
private:
	list<int> msgRecvQueue;
	mutex myMutex1;
	mutex myMutex2;
};

int main()
{
	A myobja;
	thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
	thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
	myOutMsgObj.join();
	myInMsgObj.join();
	return 0;
}

  std::adopt_lock用作第二个标记,表示这个互斥量已经被lock(),我们必须把互斥量提前lock了,否则会报异常,因为我们不需要在unique_lock的构造函数中lock这个互斥量了。(lock_guard中也可以用这个参数。)

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class A {
public:
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				myMutex1.lock(); // 要先lock之后才能用unique_lock的std::adopt_lock参数
				std::unique_lock<std::mutex> sbguard1(myMutex1, std::adopt_lock);
				msgRecvQueue.push_back(i);
			}
		}
	}
	bool outMsgLULProc(){
		std::unique_lock<std::mutex> sbguard1(myMutex1);
		if (!msgRecvQueue.empty())
		{
			cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
			msgRecvQueue.pop_front();
			return true;
		}
		return false;
	}

	void outMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			if (outMsgLULProc()){
				cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
			}
			else{
				// 消息队列为空
				cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
			}
		}
	}
private:
	list<int> msgRecvQueue;
	mutex myMutex1;
	mutex myMutex2;
};

int main()
{
	A myobja;
	thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
	thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
	myOutMsgObj.join();
	myInMsgObj.join();
	return 0;
}

  对于unique_lock的灵活性,我们可以采用std::try_to_lock代码演示。假设有如下的一个应用场景:在outMsgLULProc函数中,myMutex1这个把锁被拿走之后,卡住了20s,这导致inMsgRecvQueue函数中的锁也被卡住了20s,非常的不灵活。

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class A {
public:
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				myMutex1.lock(); // 要先lock之后才能用unique_lock的std::adopt_lock参数
				std::unique_lock<std::mutex> sbguard1(myMutex1, std::adopt_lock);
				msgRecvQueue.push_back(i);
			}
		}
	}
	bool outMsgLULProc(){
		std::unique_lock<std::mutex> sbguard1(myMutex1);
		std::chrono::milliseconds dura(20000); // 20000毫秒 = 20秒
		std::this_thread::sleep_for(dura); // 休息一定的时长
		if (!msgRecvQueue.empty())
		{
			cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
			msgRecvQueue.pop_front();
			return true;
		}
		return false;
	}

	void outMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			if (outMsgLULProc()){
				cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
			}
			else{
				// 消息队列为空
				cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
			}
		}
	}
private:
	list<int> msgRecvQueue;
	mutex myMutex1;
	mutex myMutex2;
};

int main()
{
	A myobja;
	thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
	thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
	myOutMsgObj.join();
	myInMsgObj.join();
	return 0;
}

  std::try_to_lock尝试用mutxlock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里。但是使用try_to_lock的前提是不能先去使用lock,因为如果连续调用两次mutexlock会导致程序卡死。

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class A {
public:
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				//myMutex1.lock(); // 用try_to_lock不能先lock
				std::unique_lock<std::mutex> sbguard1(myMutex1, std::try_to_lock); 
				if (sbguard1.owns_lock()) { // 如果拿到了锁
					msgRecvQueue.push_back(i);
				}
				else {
					cout << "inMsgRecvQueue函数中myMutex1没拿到锁" << endl;
				}
			}
		}
	}
	bool outMsgLULProc(){
		std::unique_lock<std::mutex> sbguard1(myMutex1);
		std::chrono::milliseconds dura(20000); // 20000毫秒 = 20秒
		std::this_thread::sleep_for(dura); // 休息一定的时长
		if (!msgRecvQueue.empty())
		{
			cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
			msgRecvQueue.pop_front();
			return true;
		}
		return false;
	}

	void outMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			if (outMsgLULProc()){
				cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
			}
			else{
				// 消息队列为空
				cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
			}
		}
	}
private:
	list<int> msgRecvQueue;
	mutex myMutex1;
	mutex myMutex2;
};

int main()
{
	A myobja;
	thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
	thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
	myOutMsgObj.join();
	myInMsgObj.join();
	return 0;
}

  使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方。前提是不能提前lock()。然后用owns_locks()方法判断是否拿到锁,如拿到返回true

  第二参数std::defer_lock,是始化了一个没有加锁的mutex,不给它加锁的目的是以后可以调用unique_lock的一些方法,使用std::defer_lock的前提是不能提前lockunique_lock的成员函数有(前三个与std::defer_lock联合使用):

  1. lock(),加锁,此方法不用自己unlock()
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class A {
public:
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				//myMutex1.lock(); // 用try_to_lock不能先lock
				std::unique_lock<std::mutex> sbguard1(myMutex1, std::defer_lock); // 创建一个没有加锁的myMutex1,然后将其和sbguard1绑定在一起
				sbguard1.lock(); // 不用自己unlock
				msgRecvQueue.push_back(i);
			}
		}
	}
	bool outMsgLULProc(){
		std::unique_lock<std::mutex> sbguard1(myMutex1);
		std::chrono::milliseconds dura(200); // 20000毫秒 = 20秒
		std::this_thread::sleep_for(dura); // 休息一定的时长
		if (!msgRecvQueue.empty())
		{
			cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
			msgRecvQueue.pop_front();
			return true;
		}
		return false;
	}

	void outMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			if (outMsgLULProc()){
				cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
			}
			else{
				// 消息队列为空
				cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
			}
		}
	}
private:
	list<int> msgRecvQueue;
	mutex myMutex1;
	mutex myMutex2;
};

int main()
{
	A myobja;
	thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
	thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
	myOutMsgObj.join();
	myInMsgObj.join();
	return 0;
}

  1. unlock():解锁。在一些场合下,因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//处理一些共享代码
myUniLock.unlock();
//处理一些非共享代码
myUniLock.lock();
//处理一些共享代码
  1. try_lock():尝试给互斥量加锁,如果拿不到锁,返回false,否则返回trueinMsgRecvQueue函数可以修改为:
void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				//myMutex1.lock(); // 用try_to_lock不能先lock
				std::unique_lock<std::mutex> sbguard1(myMutex1, std::defer_lock); // 创建一个没有加锁的myMutex1,然后将其和sbguard1绑定在一起
				if (sbguard1.try_lock() == true) {
					msgRecvQueue.push_back(i);
				}
				else {
					cout << "没有拿到锁" << endl;
				}
			}
		}
	}
  1. release():返回它所管理的mutex对象指针,并释放所有权。也就是说这个unique_lockmutex不再有关系。std::unique_lock<std::mutex> sbguard1(myMutex1);相当于把myMutex1sbguard1绑定在了一起,release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权。
void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				//myMutex1.lock(); // 用try_to_lock不能先lock
				std::unique_lock<std::mutex> sbguard1(myMutex1); // 创建一个没有加锁的myMutex1,然后将其和sbguard1绑定在一起
				std::mutex *ptx = sbguard1.release(); // 现在有责任自己解锁myMutex1了
				msgRecvQueue.push_back(i);
				ptx->unlock();
			}
		}

  所有权由ptx接管,如果原来mutex对象处理加锁状态,就需要ptx在以后进行解锁了。

  1. unique_lock所有权的传递。std::unique_lock<std::mutex> sbguard1(myMutex1);相当于把myMutex1sbguard1绑定在了一起,也就是sbguard1拥有myMutex1的所有权。sbguard1可以把自己对myMutex1的所有权转移给其它的unique_lock对象,所以unique_lock对象对myMutex1的所有权是属于,不能复制。
void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i){
			cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
			{
				//myMutex1.lock(); // 用try_to_lock不能先lock
				std::unique_lock<std::mutex> sbguard1(myMutex1); // 创建一个没有加锁的myMutex1,然后将其和sbguard1绑定在一起。
				std::unique_lock<std::mutex> sbguard2(std::move(sbguard1)); // 移动语义,现在相当于sbguard2和myMutex1绑定到了一起。
				
				msgRecvQueue.push_back(i);
				
				/*if (sbguard1.try_lock() == true) {
					msgRecvQueue.push_back(i);
				}
				else {
					cout << "没有拿到锁" << endl;
				}*/
				
			}
		}
	}

  还有一种方式,在函数中return一个临时变量,即可以实现转移:

unique_lock<mutex> aFunction()
{
    unique_lock<mutex> myUniLock(myMutex);
    //移动构造函数那里讲从函数返回一个局部的unique_lock对象是可以的
    //返回这种局部对象会导致系统生成临时的unique_lock对象,并调用unique_lock的移动构造函数
    return myUniLock;
}

参考

  • https://blog.csdn.net/qq_38231713/article/details/106091902
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值