1. 程式人生 > >C++11之多執行緒(二)

C++11之多執行緒(二)

二, 互斥物件和鎖    互斥(Mutex::Mutual Exclusion)    下面的程式碼中兩個執行緒連續的往int_set中插入多個隨機產生的整數
#include <thread>
#include <set>
#include <random>

using namespace std;

int main()
{
	std::set<int> int_set;
	auto f = [&int_set](){
		try
		{
			std::random_device rd;
			std::mt19937 gen(rd());
			std::uniform_int_distribution<> dis(1, 1000);
			for (std::size_t i = 0; i != 100000; ++i)
			{
				int_set.insert(dis(gen));
				cout << dis(gen) << endl;
			}
		}
		catch (...)
		{

		}
	};

	std::thread td1(f), td2(f);
	td1.join();
	td2.join();

	getchar();
	return 0;
}
        由於std::set::insert不是多執行緒安全的,多個執行緒同時對同一個物件呼叫insert其行為是未定義的(通常導致的結果是程式崩潰)。因此需要一種機制在此處對多個執行緒進行同步,保證任一時刻至多有一個執行緒在呼叫insert函式。        C++11提供了4個互斥物件(C++14提供了一個)用於同步多個執行緒對共享資源的訪問。
        鎖(Lock)    這裡的鎖是動詞而非名詞,互斥物件的主要操作有兩個,加鎖(lock)和釋放鎖(unlock)。當一個執行緒對互斥物件進行lock操作併成功獲得這個互斥物件的所有權,在此執行緒對此物件unlock前,其他執行緒對這個互斥物件的lock操作都會被阻塞。修改前面的程式碼在兩個執行緒中對共享資源int_set執行insert操作前先對互斥物件mt進行加鎖操作,待操作完成後再釋放鎖。這樣就能保證同一時刻至多隻有一個執行緒對int_set物件執行insert操作。
#include <thread>
#include <set>
#include <random>
#include <mutex>

using namespace std;

int main()
{
	std::mutex mt;
	std::set<int> int_set;
	auto f = [&int_set, &mt](){
		try
		{
			std::random_device rd;
			std::mt19937 gen(rd());
			std::uniform_int_distribution<> dis(1, 1000);
			for (std::size_t i = 0; i != 100000; ++i)
			{
				mt.lock();
				int_set.insert(dis(gen));
				mt.unlock();
				cout << dis(gen) << endl;
			}
		}
		catch (...)
		{

		}
	};

	std::thread td1(f), td2(f);
	td1.join();
	td2.join();

	getchar();
	return 0;
}
        使用RAII管理互斥物件        在使用鎖時應避免發生死鎖。前面的程式碼倘若一個執行緒在執行int_set.insert時丟擲異常,會導致unlock不被執行,從而可能導致另一個執行緒永遠的阻塞在第9行的lock操作。類似的情況還有比如你寫了一個函式,在進入函式後首先做的事情就是對某互斥物件執行lock操作,然而這個函式有許多的分支,並且其中有幾個分支要提前返回。因此你不得不在每個要提前返回的分支在返回前對這個互斥物件執行unlock操作。一但有某個分支在返回前忘了對這個互斥物件執行unlock,就可能會導致程式死鎖。    C++通常使用RAII來自動管理資源。如果可能應總是使用標準庫提供的互斥物件管理類模板。
    使用std::lock_guard類模板修改前面的程式碼,在lck物件構造時加鎖,析構時自動釋放鎖,即使insert丟擲了異常lck物件也會被正確的析構,所以也就不會發生互斥物件沒有釋放鎖而導致死鎖的問題。#include <thread> #include <set> #include <random> #include <mutex> using namespace std; int main() { std::mutex mt; std::set<int> int_set; auto f = [&int_set, &mt](){ try { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(1, 1000); for (std::size_t i = 0; i != 100000; ++i) { std::lock_guard<std::mutex> lck(mt); int_set.insert(dis(gen)); cout << dis(gen) << endl; } } catch (...) { } }; std::thread td1(f), td2(f); td1.join(); td2.join(); getchar(); return 0;}

互斥物件管理類模板的加鎖策略

前面提到std::lock_guard、std::unique_lock和std::shared_lock類模板在構造時是否加鎖是可選的,C++11提供了3種加鎖策略。

策略tag type描述
(預設)請求鎖,阻塞當前執行緒直到成功獲得鎖。
std::defer_lockstd::defer_lock_t不請求鎖。
std::try_to_lockstd::try_to_lock_t嘗試請求鎖,但不阻塞執行緒,鎖不可用時也會立即返回。
std::adopt_lockstd::adopt_lock_t假定當前執行緒已經獲得互斥物件的所有權,所以不再請求鎖。

下表列出了互斥物件管理類模板對各策略的支援情況。

策略std::lock_guardstd::unique_lockstd::shared_lock
(預設)√(共享)
std::defer_lock×
std::try_to_lock×
std::adopt_lock

下面的程式碼中std::unique_lock指定了std::defer_lock。

1
2
3
4
5
std::mutex mt;
std::unique_lock<std::mutex> lck(mt, std::defer_lock);
assert(lck.owns_lock() == false);
lck.lock();
assert(lck.owns_lock() == true);

對多個互斥物件加鎖

在某些情況下我們可能需要對多個互斥物件進行加鎖,考慮下面的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
std::mutex mt1, mt2;
// thread 1
{
    std::lock_guard<std::mutex> lck1(mt1);
    std::lock_guard<std::mutex> lck2(mt2);
    // do something
}
// thread 2
{
    std::lock_guard<std::mutex> lck2(mt2);
    std::lock_guard<std::mutex> lck1(mt1);
    // do something
}

如果執行緒1執行到第5行的時候恰好執行緒2執行到第11行。那麼就會出現

  • 執行緒1持有mt1並等待mt2
  • 執行緒2持有mt2並等待mt1

發生死鎖。
為了避免發生這類死鎖,對於任意兩個互斥物件,在多個執行緒中進行加鎖時應保證其先後順序是一致。前面的程式碼應修改成

1
2
3
4
5
6
7
8
9
10
11
12
13
std::mutex mt1, mt2;
// thread 1
{
    std::lock_guard<std::mutex> lck1(mt1);
    std::lock_guard<std::mutex> lck2(mt2);
    // do something
}
// thread 2
{
    std::lock_guard<std::mutex> lck1(mt1);
    std::lock_guard<std::mutex> lck2(mt2);
    // do something
}

更好的做法是使用標準庫中的std::lock和std::try_lock函式來對多個Lockable物件加鎖。std::lock(或std::try_lock)會使用一種避免死鎖的演算法對多個待加鎖物件進行lock操作(std::try_lock進行try_lock操作),當待加鎖的物件中有不可用物件時std::lock會阻塞當前執行緒知道所有物件都可用(std::try_lock不會阻塞執行緒當有物件不可用時會釋放已經加鎖的其他物件並立即返回)。使用std::lock改寫前面的程式碼,這裡刻意讓第6行和第13行的引數順序不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::mutex mt1, mt2;
// thread 1
{
    std::unique_lock<std::mutex> lck1(mt1, std::defer_lock);
    std::unique_lock<std::mutex> lck2(mt2, std::defer_lock);
    std::lock(lck1, lck2);
    // do something
}
// thread 2
{
    std::unique_lock<std::mutex> lck1(mt1, std::defer_lock);
    std::unique_lock<std::mutex> lck2(mt2, std::defer_lock);
    std::lock(lck2, lck1);
    // do something
}

此外std::lock和std::try_lock還是異常安全的函式(要求待加鎖的物件unlock操作不允許丟擲異常),當對多個物件加鎖時,其中如果有某個物件在lock或try_lock時丟擲異常,std::lock或std::try_lock會捕獲這個異常並將之前已經加鎖的物件逐個執行unlock操作,然後重新丟擲這個異常(異常中立)。並且std::lock_guard的建構函式lock_guard(mutex_type& m, std::adopt_lock_t t)也不會丟擲異常。所以std::lock像下面這麼用也是正確

1
2
3
std::lock(mt1, mt2);
std::lock_guard<std::mutex> lck1(mt1, std::adopt_lock);
std::lock_guard<std::mutex> lck2(mt2, std::adopt_lock);