C++11多线程 互斥量的概念、用法、死锁演示及解决详解

x33g5p2x  于2022-03-06 转载在 其他  
字(9.3k)|赞(0)|评价(0)|浏览(310)

1.互斥量(mutex)的基本概念

保护共享数据,操作时,某个线程用代码把共享数据锁住、然后操作数据、最后解锁;其它想操作共享数据的线程必须等待解锁;

互斥量是个类对象,理解成一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁成功(成功的标志是lock()函数返回);如果没锁成功,那么这个流程卡在lock(),不断的尝试去锁这把锁头;

互斥量使用要小心,保护数据不多也不能少,少了,没有达到保护的效果,多了,影响效率;

2.互斥量的用法

2.1 lock()、unlock()

步骤:先lock(),操作共享数据,unlock()

lock()与unlock()要成对使用,有lock()必然要有unlock(),每调用一次lock(),必然应该调用一次unlock();不应该也不允许调用一次lock(),却调用了2次unlock(),这些非对称数量的调用都会导致代码不稳定甚至崩溃。实例代码如下:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

//准备用成员函数作为线程函数的方法写线程
class A
{
public:
	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue() 
	{
		for (int i = 0; i < 10000;i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素"<< i<<endl;
			my_mutex.lock();
			msgRecvQueue.push_back(i); //假设这个数字i就是收到的命令,直接弄到消息队列里边来;
			my_mutex.unlock();
		}
	}

	bool outMsgLULProc(int &command)
	{
		my_mutex.lock();
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
			msgRecvQueue.pop_front();//移除第一个元素。但不返回;
			my_mutex.unlock();  //所有分支都必须有unlock()
			return true;
		}
		my_mutex.unlock();
		return false;
	}
	//把数据从消息队列取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 10000; i++)
		{
			bool result = outMsgLULProc(command);

			if (result  == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素"<< endl;
				//处理数据
			}
			else
			{
				//消息队列为空
				cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl;
			}
		}
		cout <<"end!" << endl;
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;
};

int main()
{
	A myobja;

	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//第二个参数,引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);

	myOutMsgObj.join();
	myInMsgObj.join();

	cout << "主线程执行!" << endl;

	return 0;
}

注:有lock(),忘记unlock()的问题很难排查;

为了防止大家忘记unlock(),引入了一个叫std::lock_guard的类模板:忘记unlock(),替你unlock()。如同智能指针(unique_ptr<>),你忘记释放内存不要紧,我替你释放。

2.2 std::lock_guard类模板

直接取代lock()与unlock();也就是说,你用了lock_guard之后,再不能使用lock()和unlock()。std::lock_guard类模板的原理很简单,lock_guard构造函数里执行了mutex::lock();ock_guard析构函数里执行了mutex::unlock()。实例代码如下:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

//准备用成员函数作为线程函数的方法写线程
class A
{
public:
	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
			{  //大括号提前结束lock_guard生命周期
				std::lock_guard<std::mutex> sbguard(my_mutex); 
				//my_mutex.lock();
				msgRecvQueue.push_back(i); //假设这个数字i就是收到的命令,直接弄到消息队列里边来;
				//my_mutex.unlock();
			}
		}
	}

	bool outMsgLULProc(int &command)
	{
		std::lock_guard<std::mutex> sbguard(my_mutex);//sbguard时对象名
		//lock_guard构造函数里执行了mutex::lock()
		//lock_guard析构函数里执行了mutex::unlock()
		//my_mutex.lock();
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
			msgRecvQueue.pop_front();//移除第一个元素。但不返回;
			//my_mutex.unlock();  //所有分支都必须有unlock()
			return true;
		}
		//my_mutex.unlock();
		return false;
	}
	//把数据从消息队列取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 10000; i++)
		{
			bool result = outMsgLULProc(command);

			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//处理数据
			}
			else
			{
				//消息队列为空
				cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl;
			}
		}
		cout << "end!" << endl;
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;//创建一个互斥量(一把锁)
};

int main()
{
	A myobja;

	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//第二个参数,引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);

	myOutMsgObj.join();
	myInMsgObj.join();

	cout << "主线程执行!" << endl;

	return 0;
}

3.死锁

通俗解释:

张三:站在北京,等李四,不挪窝;

李四:站在深圳,等张三,不挪窝;

C++ 中:

比如有两把锁(死锁问题,是由至少两把锁头[两个互斥量才能] 产生);

问题分析:

两个线程A、B;两把锁:金锁(jinlock),银锁(yinlock)

(1)线程A执行的时候,这个线程先锁,把金锁lock()成功了,然后它去lock银锁。。。

(2)此时出现了上下文切换

(3)线程B执行了,这个线程先锁银锁,因为银锁还没被锁,所以银锁会lock()成功,线程B要去lock()金锁。。。

(4)此时此刻,死锁就产生了;

(5)线程A因为拿不到银锁头,流程走不下去(所有后边代码有解金锁的但是流程走不下去,所以金锁解不开)

(6)线程B因为拿不到金锁头,流程走不下去(所有后边代码有解银锁的但是流程走不下去,所以银锁解不开)

大家都晾在那里,你等我,我等你

3.1 死锁演示

两个线程上锁的顺序正好是反着的。 实例代码如下:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

//准备用成员函数作为线程函数的方法写线程
class A
{
public:
	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;

			my_mutex1.lock();//实际工程中,这两个锁并不定挨着,可能他们需要保护不同的数据共享块
			my_mutex2.lock();
			msgRecvQueue.push_back(i); //假设这个数字i就是收到的命令,直接弄到消息队列里边来;
			my_mutex2.unlock();
			my_mutex1.unlock();
		}
	}

	bool outMsgLULProc(int &command)
	{
		//std::lock_guard<std::mutex> sbguard1(my_mutex1);
		//std::lock_guard<std::mutex> sbguard2(my_mutex2);

		my_mutex2.lock();  //
		my_mutex1.lock();
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
			msgRecvQueue.pop_front();//移除第一个元素。但不返回;
			my_mutex2.unlock();
			my_mutex1.unlock(); //所有分支都必须有unlock()
			return true;
		}
		my_mutex2.unlock();
		my_mutex1.unlock();
		return false;
	}
	//把数据从消息队列取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 10000; i++)
		{
			bool result = outMsgLULProc(command);

			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//处理数据
			}
			else
			{
				//消息队列为空
				cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl;
			}
		}
		cout << "end!" << endl;
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex1;//创建一个互斥量(一把锁)
	std::mutex my_mutex2;//创建一个互斥量
};

int main()
{
	A myobja;

	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//第二个参数,引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);

	myOutMsgObj.join();
	myInMsgObj.join();

	cout << "主线程执行!" << endl;

	return 0;
}

3.2 死锁的一般解决方案

只要保证这两个互斥量上锁的顺序一致就不会死锁

3.3 std::lock()函数模板

用来处理多个互斥量的时候才出场

能力:一次锁住两个或者两个以上的互斥量(至少两个,多了不限);它不存在这种因为多个线程中因为锁的顺序问题导致死锁的风险问题;

std::lock():如果互斥量中有一个没有锁住,它就在那等着,等所有互斥量都锁住,它才能往下走(返回);要么两个互斥量都锁柱,要么两个互斥量都没锁住,如果只锁了一个,另外一个没有锁成功,则它立即把已经锁住的解锁。实例代码如下:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

//准备用成员函数作为线程函数的方法写线程
class A
{
public:
	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;

			std::lock(my_mutex1, my_mutex2);//相当于每个互斥量都调用了.lock()

			msgRecvQueue.push_back(i); //假设这个数字i就是收到的命令,直接弄到消息队列里边来;
			my_mutex2.unlock();
			my_mutex1.unlock();
		}
	}

	bool outMsgLULProc(int &command)
	{
		std::lock(my_mutex1, my_mutex2);
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
			msgRecvQueue.pop_front();//移除第一个元素。但不返回;
			my_mutex2.unlock();
			my_mutex1.unlock(); //所有分支都必须有unlock()
			return true;
		}
		my_mutex2.unlock();
		my_mutex1.unlock();
		return false;
	}
	//把数据从消息队列取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 10000; i++)
		{
			bool result = outMsgLULProc(command);

			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//处理数据
			}
			else
			{
				//消息队列为空
				cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl;
			}
		}
		cout << "end!" << endl;
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex1;//创建一个互斥量(一把锁)
	std::mutex my_mutex2;//创建一个互斥量
};

int main()
{
	A myobja;

	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//第二个参数,引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);

	myOutMsgObj.join();
	myInMsgObj.join();

	cout << "主线程执行!" << endl;

	return 0;
}

3.4 std::lock_guard()的std::adopt_lock参数

std::adopt_lock是个结构体对象,起一个标志作用:就是表示这个互斥量已经lock(),不需要在std::lock_guardstd::mutex构造函数里再对mutex对象进行再次lock()了。实例代码如下:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>

using namespace std;

//准备用成员函数作为线程函数的方法写线程
class A
{
public:
	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;

			std::lock(my_mutex1, my_mutex2);//相当于每个互斥量都调用了.lock()

			std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);
			std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock);

			msgRecvQueue.push_back(i); //假设这个数字i就是收到的命令,直接弄到消息队列里边来;

		}
	}

	bool outMsgLULProc(int &command)
	{
		std::lock(my_mutex1, my_mutex2);

		std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);
		std::lock_guard<std::mutex> sbguard2(my_mutex2, std::adopt_lock);

		if (!msgRecvQueue.empty())
		{
			//消息不为空
			int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
			msgRecvQueue.pop_front();//移除第一个元素。但不返回;

			return true;
		}

		return false;
	}
	//把数据从消息队列取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 10000; i++)
		{
			bool result = outMsgLULProc(command);

			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//处理数据
			}
			else
			{
				//消息队列为空
				cout << "inMsgRecvQueue()执行,但目前消息队列中为空!" << i << endl;
			}
		}
		cout << "end!" << endl;
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex1;//创建一个互斥量(一把锁)
	std::mutex my_mutex2;//创建一个互斥量
};

int main()
{
	A myobja;

	std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//第二个参数,引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);

	myOutMsgObj.join();
	myInMsgObj.join();

	cout << "主线程执行!" << endl;

	return 0;
}

总结:std::lock()一次锁定多个互斥量;谨慎使用(建议一个一个锁),因为不同的互斥量控制不同的共享数据,两个互斥量在一起的情况不多见。

注:该文是C++11并发多线程视频教程笔记,详情学习:https://study.163.com/course/courseMain.htm?courseId=1006067356

相关文章