17.5 互斥量的概念、用法、死锁演示与解决详解

保护共享数据,操作时,某个线程,用代码把共享数据锁住,操作数据,解锁
其他操作共享数据的线程必须等待解锁。锁定住,操作,解锁。

一、互斥量(mutex)的基本概念

互斥量是个类对象,理解成一把锁,多个线程尝试用 lock() 成员函数来加锁这把锁头,只有一个线程能锁定成功(成功的标注是lock函数返回)。
如果没有锁成功,那么流程卡在 lock() 这里不断的尝试去锁这把锁头。
互斥量使用要小心,保护数据不能多也不能少,少了没有达到保护效果,多了影响效率。

二、互斥量的用法

<1>lock()、unlock()
先 lock() 操作共享数据,再 unlock()
lock() 和 unlock() 要成对使用,有 lock() 必然要有 unlock(),每调用一次 lock(),必然应该调用一次unlock()。
不应该也不允许调用1次 lock() 却调用了2次 unlock(),也不允许调用2次 lock() 却调用1次 unlock(),
这些非对称数量的调用都会导致代码不稳定甚至崩溃。

class MA
{
public:
	//把收到的消息(玩家命令)放入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
			my_mutex.lock();
			msgRecvQueue.push_back(i);  //假设这个数字i就是我收到的命令,直接弄到消息队列中
			my_mutex.unlock();
		}
	}

	bool outMsgLUProc(int& command)
	{
		my_mutex.lock();
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			command = msgRecvQueue.front();  //返回第一个元素,但不检查元素是否存在;
			msgRecvQueue.pop_front();  //移除第一个元素,但不返回
			my_mutex.unlock();
			return true;
		}
		my_mutex.unlock();
		return false;
	}

	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			if (!msgRecvQueue.empty())
			{
				outMsgLUProc(i);
				//这里就考虑处理数据...
				//...
			}
			else
			{
				//消息队列为空
				cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
			}
		}
		cout << "end" << endl;
	}
private:
	list<int> msgRecvQueue;  //容器(消息队列),专门用于代表玩家发送的命令
	std::mutex my_mutex;  //创建了一个互斥量
};

int main()
{
	MA myobj;
	std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj);  //第二个参数是引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
	myOutMsgObj.join();
	myInMsgObj.join();
	cout << "main主函数执行结束" << endl;  //最后执行这句,整个进程退出
	return 0;
}

有 lock(),忘记 unlock() 的问题,非常难排查;
为了防止大家忘记 unlock(),引入一个叫 std::lock_guard 的类模板:你忘记 unlock() 不要紧,我替你unlock();
学习过智能指针(unique_ptr<>):你忘记释放内存不要紧,我给你释放; 保姆

<2>std::lock_guard类模板:直接取代 lock() 和 unlock(),也就是说,你用了 lock_gurad 之后,再不能使用 lock() 和 unlock()。

class MA
{
public:
	//把收到的消息(玩家命令)放入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
			{
				lock_guard<std::mutex> sbguard(my_mutex);
				msgRecvQueue.push_back(i);  //假设这个数字i就是我收到的命令,直接弄到消息队列中
			}
		}
	}

	bool outMsgLUProc(int& command)
	{
		lock_guard<std::mutex> sbguard(my_mutex);  //sbguard是随便起的对象名
		//lock_guard构造函数里执行了mutex::lock();
		//lock_guard析构函数里执行了mutex::unlock();
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			command = msgRecvQueue.front();  //返回第一个元素,但不检查元素是否存在;
			msgRecvQueue.pop_front();  //移除第一个元素,但不返回
			return true;
		}
		return false;
	}

	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLUProc(command);
			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//这里就考虑处理数据...
				//...
			}
			else
			{
				cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
			}
		}
		cout << "end" << endl;
	}
private:
	list<int> msgRecvQueue;  //容器(消息队列),专门用于代表玩家发送的命令
	std::mutex my_mutex;  //创建了一个互斥量
};

int main()
{
	MA myobj;
	std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj);  //第二个参数是引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
	myOutMsgObj.join();
	myInMsgObj.join();
	cout << "main主函数执行结束" << endl;  //最后执行这句,整个进程退出
	return 0;
}

三、死锁

张三:站在北京等李四 不挪窝
李四:站在深圳等张三 不挪窝
C++中,比如我有两把锁(死锁这个问题是由至少两个锁头也就是两个互斥量才能产生),金锁(JinLock),银锁(YinLock);
两个线程A,B

1>线程A执行的时候,这个线程先锁金锁,把金锁lock()成功后,然后它去lock()银锁…
出现上下文切换
2>线程B执行了,这个线程先锁银锁,因为银锁还没有被锁,所以银锁会lock()成功,线程B要去lock()金锁…
此时此刻,死锁就产生了
3>线程A因为拿不到银锁,流程走不下去(所有后边代码有解锁金锁的但是流程走不下去,所以金锁解不开)
4>线程B因为拿不到金锁,流程走不下去(所有后边代码有解锁银锁的但是流程走不下去,所以银锁解不开)
大家都晾在这里,你等我,我等你

<1>死锁演示

class MA
{
public:
	//把收到的消息(玩家命令)放入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
			{
				my_mutex_1.lock();  //实际工程这两个锁头不一定挨着,可能他们需要保护不同的数据共享块
				my_mutex_2.lock();
				msgRecvQueue.push_back(i);  //假设这个数字i就是我收到的命令,直接弄到消息队列中
				my_mutex_2.unlock();
				my_mutex_1.unlock();
			}
		}
	}

	bool outMsgLUProc(int& command)
	{
		my_mutex_2.lock();
		my_mutex_1.lock();
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			command = msgRecvQueue.front();  //返回第一个元素,但不检查元素是否存在;
			msgRecvQueue.pop_front();  //移除第一个元素,但不返回
			my_mutex_1.unlock();
			my_mutex_2.unlock();
			return true;
		}
		my_mutex_1.unlock();
		my_mutex_2.unlock();
		return false;
	}

	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLUProc(command);
			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//这里就考虑处理数据...
				//...
			}
			else
			{
				cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
			}
		}
		cout << "end" << endl;
	}
private:
	list<int> msgRecvQueue;  //容器(消息队列),专门用于代表玩家发送的命令
	mutex my_mutex_1;  //创建了一个互斥量  (一把锁头)
	mutex my_mutex_2;  //创建了一个互斥量
};

int main()
{
	MA myobj;
	std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj);  //第二个参数是引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
	myOutMsgObj.join();
	myInMsgObj.join();
	cout << "main主函数执行结束" << endl;  //最后执行这句,整个进程退出
	return 0;
}

<2>死锁的一般解决方案
只要保证这两个互斥量上锁的顺序一致就不会死锁

class MA
{
public:
	//把收到的消息(玩家命令)放入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
			{
				std::lock_guard<std::mutex> guard_1(my_mutex_1);
				std::lock_guard<std::mutex> guard_2(my_mutex_2);
				msgRecvQueue.push_back(i);  //假设这个数字i就是我收到的命令,直接弄到消息队列中
			}
		}
	}

	bool outMsgLUProc(int& command)
	{
		std::lock_guard<std::mutex> guard1(my_mutex_1);
		std::lock_guard<std::mutex> guard2(my_mutex_2);
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			command = msgRecvQueue.front();  //返回第一个元素,但不检查元素是否存在;
			msgRecvQueue.pop_front();  //移除第一个元素,但不返回
			return true;
		}
		return false;
	}

	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLUProc(command);
			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//这里就考虑处理数据...
				//...
			}
			else
			{
				cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
			}
		}
		cout << "end" << endl;
	}
		cout << "end" << endl;
	}
private:
	list<int> msgRecvQueue;  //容器(消息队列),专门用于代表玩家发送的命令
	mutex my_mutex_1;  //创建了一个互斥量  (一把锁头)
	mutex my_mutex_2;  //创建了一个互斥量
};

int main()
{
	MA myobj;
	std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj);  //第二个参数是引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
	myOutMsgObj.join();
	myInMsgObj.join();
	cout << "main主函数执行结束" << endl;  //最后执行这句,整个进程退出
	return 0;
}

<3>std::lock()函数模板:用来处理多个互斥量的时候才出场
能力:一次锁住两个或者两个以上的互斥量 (至少2个,多了不限,1个不行)。
它不存这种因为在多个线程中,因为锁的顺序问题导致死锁的风险。
std::lock():如果互斥量中有一个没锁住,它就在那里等着,等所有互斥量都锁住,它才能往下走(返回)。
要么两个互斥量都锁住,要么两个互斥量都没锁住。如果只锁了一个,另外一个没锁成功,则它立即把已经锁住的解锁。

class MA
{
public:
	//把收到的消息(玩家命令)放入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
			{
				std::lock(my_mutex_1, my_mutex_2);  //相当于每个互斥量都调用了lock()
				msgRecvQueue.push_back(i);  //假设这个数字i就是我收到的命令,直接弄到消息队列中
				my_mutex_2.unlock();
				my_mutex_1.unlock();
			}
		}
	}

	bool outMsgLUProc(int& command)
	{
		std::lock(my_mutex_2, my_mutex_1);
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			command = msgRecvQueue.front();  //返回第一个元素,但不检查元素是否存在;
			msgRecvQueue.pop_front();  //移除第一个元素,但不返回
			my_mutex_1.unlock();
			my_mutex_2.unlock();
			return true;
		}
		my_mutex_1.unlock();
		my_mutex_2.unlock();
		return false;
	}

	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLUProc(command);
			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//这里就考虑处理数据...
				//...
			}
			else
			{
				cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
			}
		}
		cout << "end" << endl;
	}
private:
	list<int> msgRecvQueue;  //容器(消息队列),专门用于代表玩家发送的命令
	mutex my_mutex_1;  //创建了一个互斥量  (一把锁头)
	mutex my_mutex_2;  //创建了一个互斥量
};

int main()
{
	MA myobj;
	std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj);  //第二个参数是引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
	myOutMsgObj.join();
	myInMsgObj.join();
	cout << "main主函数执行结束" << endl;  //最后执行这句,整个进程退出
	return 0;
}

<4>std::lock_guard的std::lock参数
std::adopt_lock是个结构体对象,起一个标记作用,作用就是表示这个互斥量已经lock();
不需要在std::lock_guardstd::mutex构造函数里面再对mutex对象进行lock()了。

class MA
{
public:
	//把收到的消息(玩家命令)放入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; ++i)
		{
			cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
			{
				std::lock(my_mutex_1, my_mutex_2);  //相当于每个互斥量都调用了lock()
				std::lock_guard<std::mutex> guard_1(my_mutex_1, std::adopt_lock);
				std::lock_guard<std::mutex> guard_2(my_mutex_2, std::adopt_lock);
				msgRecvQueue.push_back(i);  //假设这个数字i就是我收到的命令,直接弄到消息队列中
			}
		}
	}

	bool outMsgLUProc(int& command)
	{
		std::lock(my_mutex_2, my_mutex_1);
		std::lock_guard<std::mutex> guard1(my_mutex_2, std::adopt_lock);
		std::lock_guard<std::mutex> guard2(my_mutex_1, std::adopt_lock);
		if (!msgRecvQueue.empty())
		{
			//消息不为空
			command = msgRecvQueue.front();  //返回第一个元素,但不检查元素是否存在;
			msgRecvQueue.pop_front();  //移除第一个元素,但不返回
			return true;
		}
		return false;
	}

	//把数据从消息队列中取出的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		for (int i = 0; i < 100000; ++i)
		{
			bool result = outMsgLUProc(command);
			if (result == true)
			{
				cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
				//这里就考虑处理数据...
				//...
			}
			else
			{
				cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
			}
		}
		cout << "end" << endl;
	}
private:
	list<int> msgRecvQueue;  //容器(消息队列),专门用于代表玩家发送的命令
	mutex my_mutex_1;  //创建了一个互斥量  (一把锁头)
	mutex my_mutex_2;  //创建了一个互斥量
};

int main()
{
	MA myobj;
	std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj);  //第二个参数是引用,才能保证线程里用的是同一个对象
	std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
	myOutMsgObj.join();
	myInMsgObj.join();
	cout << "main主函数执行结束" << endl;  //最后执行这句,整个进程退出
	return 0;
}

总结:std::lock():一次锁定多个互斥量,谨慎使用(建议一个一个锁)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值