【C++11多執行緒入門教程】系列之互斥量mutex

Small_Munich發表於2020-11-12
互斥量

  談及C++11多執行緒裡面的互斥量,首先浮現在腦海的是互斥量是有什麼用,怎麼用?下面簡短介紹一下互斥量,有什麼用?用來解決多執行緒對共享資料的訪問(讀與寫)機制,簡單說就是實現多個執行緒間對資源的互斥訪問。怎麼用?C++11裡面的互斥量相關的使用方法比較多樣化,我們本章先學習mutex的方式。

程式碼演示

我們先看下面這段沒有加鎖的問題程式碼:

  我們將下面程式碼的①②③④取消註釋,加上鎖後程式就正常執行。這部分程式碼簡單說明了多執行緒對資料的讀與取時候需要加鎖。訂票執行緒與出票執行緒函式,問題主要在於假設某一時刻訂票執行緒正準備新增一個票,還未結束。這個時刻另外一個執行緒需要進行出票,這樣會造成資料的競爭。導致程式崩潰。

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

class Tickets
{
public:
	// 訂票執行緒
	void book_tickets()
	{
		for (int i = 0; i < 1000000; i++)
		{
			std::cout << "訂票開始,該執行緒執行:" << i << std::endl;
			//mtx_tickets_.lock();  // ①
			tickets_.push_back(i);
			//mtx_tickets_.unlock(); // ②
		}
		std::cout << "訂票執行緒執行完畢." << std::endl;
	}
	// 出票執行緒
	void issue_tickets()
	{
		for (int i = 0; i < 1000000; i++)
		{
			if (!tickets_.empty()) // 訂票量不為空
			{
				//mtx_tickets_.lock();  // ③
				int ticket = tickets_.front();
				tickets_.pop_front();
				//mtx_tickets_.unlock();  // ④
			}
			else
			{
				std::cout << "訂票佇列裡面為空:" << i << std::endl;
			}
		}
		std::cout << "出票執行緒執行完畢." << std::endl;
	}

private:
	std::list<int> tickets_;
	std::mutex mtx_tickets_;
};

int main(void)
{
	Tickets tickets;

	std::thread bookTickets(&Tickets::book_tickets, &tickets);
	std::thread issueTickets(&Tickets::issue_tickets, &tickets);

	bookTickets.join();
	issueTickets.join();

	std::cout << "主執行緒執行結束." << std::endl;

	system("pause");
	return 0;
}

上面程式,我們簡單的使用mutex進行演示互斥鎖的用途:

  • 使用std::mutex宣告一個類物件,呼叫lock()與unlock()對執行緒進行上鎖,避免多個執行緒產生競爭機制。
  • 呼叫類成員函式的執行緒使用。std::thread bookTickets(&Tickets::book_tickets, &tickets);
死鎖

  我們知道多個執行緒對共享資料進行訪問時候新增互斥量會有效的避免資料競爭的問題,但是也會出現死鎖的情況。死鎖:執行緒卡死,無法繼續有效執行。舉例:看下面示意圖。

【C++11多執行緒入門教程】系列之互斥量mutex

  上圖中展示死鎖的關鍵錯誤處,主要在於不同互斥量上鎖的順序不一致導致的死鎖現象。下面我們簡單說下死鎖的流程:

  • book_tickets函式執行緒執行,mtx_tickets_0互斥量對該執行緒上鎖,搶到執行資源,但是此時CPU進行上下文切換…
  • issue_tickets函式執行緒開始執行,mtx_tickets_1互斥量對該執行緒上鎖,搶到繼續執行的資源…
  • 這個時候,mtx_tickets_0互斥量對訂票執行緒函式繼續向下執行,需要繼續上鎖mts_tickets_1,無法繼續執行因為取票執行緒的mtx_tickets_1還未解鎖,同時mtx_tickets_1互斥量對取票執行緒函式繼續向下執行,需要加鎖mtx_tickets_0,同樣無法繼續執行因為訂票執行緒的mtx_tickets_0還未進行解鎖。如此,就會導致執行緒卡死在這裡。

  通過有序的對互斥量上鎖來解決這個問題。下面為錯誤程式碼的示例,解決下面程式碼的問題,只需要將①③的順序一致即可。或者我們可以直接使用std::lock()函式。並且std::lock(mtx_tickets_0, mtx_tickets_1)裡面的互斥量物件的順序不需要對齊。

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

class Tickets
{
public:
	// 訂票執行緒
	void book_tickets()
	{
		for (int i = 0; i < 1000000; i++)
		{
			std::cout << "訂票開始,該執行緒執行:" << i << std::endl;
			mtx_tickets_0.lock();  // ①
			mtx_tickets_1.lock();  // ①
			tickets_.push_back(i);
			mtx_tickets_0.unlock(); // ②
			mtx_tickets_1.unlock(); // ②
		}
		std::cout << "訂票執行緒執行完畢." << std::endl;
	}
	// 出票執行緒
	void issue_tickets()
	{
		for (int i = 0; i < 1000000; i++)
		{
			if (!tickets_.empty()) // 訂票量不為空
			{
				mtx_tickets_1.lock();   ③
				mtx_tickets_0.lock();   ③
				int ticket = tickets_.front();
				tickets_.pop_front();
				mtx_tickets_1.unlock();  // ④
				mtx_tickets_0.unlock();  // ④
			}
			else
			{
				std::cout << "訂票佇列裡面為空:" << i << std::endl;
			}
		}
		std::cout << "出票執行緒執行完畢." << std::endl;
	}

private:
	std::list<int> tickets_;
	std::mutex mtx_tickets_0;
	std::mutex mtx_tickets_1;
};

int main(void)
{
	Tickets tickets;

	std::thread bookTickets(&Tickets::book_tickets, &tickets);
	std::thread issueTickets(&Tickets::issue_tickets, &tickets);

	bookTickets.join();
	issueTickets.join();

	std::cout << "主執行緒執行結束." << std::endl;

	system("pause");
	return 0;
}
小結

  互斥量為多執行緒進行資料訪問競爭提供一種有效的解決辦法。但是使用互斥量mutex時候需要注意一下問題:

  • 使用mutex時候,先lock()鎖住資料,操作共享的資料,然後unlock()解鎖。
  • 記住lock()加鎖與unlock()解鎖一定要成對使用,這裡的成對使用是lock()加鎖之後,千萬記得unlock()。特別會在條件判斷過程中,unlock()解鎖一定要保證任何流程執行下來都與之前的lock()對應。
  • 為了避免使用mutex時候發生死鎖,建議上鎖時候互斥量的順序一致。
  • 如果多個執行緒對共享資料都只讀的話,那麼不需要使用mutex;但是,如果存在讀與寫,那麼一定使用互斥量進行操作,避免錯誤發生。同理
  • 關於使用mutex進行加鎖lock()與解鎖unlock()的位置也是需要仔細考慮的,例如上面的程式,如果你把訂票執行緒加鎖lock()與解鎖unlock()在for迴圈外面,出票執行緒,那麼整個程式的效率將會降低(這裡我們幾乎忽略上下文切換的耗時)。
  • 避免死鎖產生,建議對上鎖的互斥量順序儘量保持一致。或者直接使用std::lock()函式來對多個互斥量物件上鎖出進行操作。

  上面總結了mutex的一些使用策略與避坑方法,當然lock()與unlock()成對使用難免會忘記,C++11多執行緒還提供了lock_guard類上鎖機制來降低這個mutex的程式碼寫錯的可能性。但是,mutex的靈活性更高,控制上鎖的區域更加靈活。lock_guard就稍微遜色些mutex。

參考

https://zh.cppreference.com/w/cpp/thread/mutex
https://www.jianshu.com/p/ce782ac3150b

相關文章