C++11多線程 互斥量的概念、用法、死鎖演示及解決詳解

目錄

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

2.互斥量的用法

2.1 lock()、unlock()

2.2 std::lock_guard類模板

3.死鎖

3.1 死鎖演示

3.2 死鎖的一般解決方案

3.3 std::lock()函數模板

3.4 std::lock_guard()的std::adopt_lock參數


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_guard<std::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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章