在上一篇《多執行序之間的溝通(一)》裡,Heresy 已經大概介紹過,最簡單的 mutex 和 lock_guard 的機制了。不過,在該篇文章裡面提到的,只是最簡單的應用;在這邊,Heresy 會繼續在講一些這方面的進階功能。
其他種類的 mutex
首先,前面也有提到,除了基本的 std::mutex 外,實際上 STL Thread 裡面,也還有提供其他型別的 mutex 類別,包括了 timed_mutex、recursive_mutex、recursive_timed_mutex;實際上,這些延伸的 mutex 類別,就是如同字面上的意思,擁有支援「定時」、「遞迴」特性的 mutex。
像是以 timed_mutex 來說,他除了基本款 mutex 的 lock() 和 try_lock() 外,還額外提供了 try_lock_for() 和 try_lock_until() 這兩個函式,可以設定在指定的時間內、試著去進行 mutex 的鎖定的動作。(參考)
而 recursive_mutex 的話,則是可以讓 mutex 認得鎖住自己的執行序,並且讓 mutex 在已經被鎖定的情況下,還是可以讓同一個執行序再去鎖定他、而不會被擋下來。下面就是一個簡單的例子:
class ClassA { public: void func1() { lock_guard<recursive_mutex> lock(mMutex); } void func2() { lock_guard<recursive_mutex> lock(mMutex); func1(); } private: recursive_mutex mMutex; };
在 ClassA 裡,有 func1() 和 func2() 兩個函式,兩者都會去建立一個 lock_guard 的物件、來鎖定 mMutex 這個 mutex。不過,由於 func2() 裡、會去呼叫 func1(),所以如果去呼叫 func2() 的話,實際上可以發現,程式會在執行 func2() 的時候,透過 lock_guard 去鎖定 mMutex,而當在 func2() 內去呼叫 func1() 的時候,同樣的要求鎖定動作,又會再進行一次!
這時候如果是使用標準的 mutex 的話,在第二次試圖去鎖定 mMutex 的時候(func1()),會因為 mMutex 已經在 func2() 裡被鎖定了,而就因此停在這邊,等待 mMutex 被解除鎖定再繼續;但是由於 mMutex 的鎖定狀態必須要等到 func2() 整個執行完成後才會解除,所以這邊就會變成永遠等不完的狀況,讓程式無法繼續執行。
但是因為這邊用的是 recursive_mutex,所以在第二次、也就是在 func1() 裡試著去鎖定 mMutex 的時候,系統會判斷出目前是由同一個執行序所鎖定的,所以就讓它繼續執行下去、不會出問題。
而最後的 recursive_timed_mutex,顧名思義,就是同時具有 timed_mutex、recursive_mutex 兩種特殊性質的 mutex 類別了~
更彈性的 unique_lock
除了 mutex 有四種不同的類型外,其實在 STL Thread 裡,除了 lock_guard 可以用來做 mutex 的自動管理外,還有另一個 unique_lock(參考),也是用來做類似的工作的,而且 unique_lock 在使用上的彈性,會比 lock_guard 來的大。
和 lock_guard 相比,unique_lock 主要的特色在於:
-
unique_lock 不一定要擁有 mutex,所以可以透過 default constructor 建立出一個空的 unique_lock。
-
unique_lock 雖然一樣不可複製(non-copyable),但是它是可以轉移的(movable)。所以,unique_lock 不但可以被函式回傳,也可以放到 STL 的 container 裡。
這兩點,都是 lock_guard 所缺乏的能力。另外,unique_lock 也有提供 lock()、unlock() 等函式,可以用來手動鎖定、解鎖 mutex,也算是功能比較完整的地方。
同時鎖定多個 mutex
雖然一般狀況下,大多是一次去鎖定一個 mutex 來使用的。但是在實際使用的時候,有的時候還是會需要同時去鎖定多個 mutex,來避免資料不同步的問題。例如,在 cppreference 就有提出一個銀行帳號例子(網頁),在這種狀況,就必須要同時鎖定「轉出者」和「轉入者」兩者個 mutex,否則會有問題。
不過由於他的範例程式在 VC11 無法正確地編譯(gcc 4.6.3 好像也不行…),所以 Heresy 自己做了一些修改:
#include <mutex> #include <thread> #include <chrono> #include <iostream> #include <string> using namespace std; struct bank_account { explicit bank_account(string name, int money) { sName = name; iMoney = money; } string sName; int iMoney; mutex mMutex; }; void transfer( bank_account &from, bank_account &to, int amount ) { // don't actually take the locks yet unique_lock<mutex> lock1( from.mMutex, defer_lock ); unique_lock<mutex> lock2( to.mMutex, defer_lock ); // lock both unique_locks without deadlock lock( lock1, lock2 ); from.iMoney -= amount; to.iMoney += amount; // output log cout << "Transfer " << amount << " from "
<< from.sName << " to " << to.sName << endl; } int main() { bank_account Account1( "User1", 100 ); bank_account Account2( "User2", 50 ); thread t1( [&](){ transfer( Account1, Account2, 10 ); } ); thread t2( [&](){ transfer( Account2, Account1, 5 ); } ); t1.join(); t2.join(); }
由於在進行轉帳(transfer())的時候,必須要先鎖定兩個帳號,然後再做數值的修改,所以這邊的範例,是在 transfer() 裡,先透過 unique_lock 來管理兩個帳號的 mutex;不過要注意的是,比較不一樣的地方,是在建立 unique_lock 物件的時候,他還加上了第二個參數 defer_lock~
這個參數的目的,是告訴 unique_lock 雖然要讓他去管理指定的 mutex 物件,但是不要立刻去鎖定他、而是維持沒有鎖定的狀態。而接下來,則是再透過 lock() 這個函式,來同時鎖定 lock1 和 lock2 這兩個 unique_lock 物件。
為什麼需要這樣做,而不直接寫成:
lock_guard<mutex> lock1( from.mMutex ); lock_guard<mutex> lock2( to.mMutex );
呢?因為如果用上面的寫法,直接依序各自鎖定兩個 mutex 的話,有可能會在多執行序的情況下,產生 dead lock 的狀況。
舉例來說,如果同時要求進行「由 A 轉帳給 B」(thread 1)和「由 B 轉帳給 A」(thread 2)的動作的話,有可能會產生一個狀況,就是在 thread 1 裡面已經鎖定了 A 的 mutex,但是在試圖鎖定 B 的 mutex 的時候,thread 2 已經鎖定了 B 的 mutex;而同樣地,這時候 thread 2 也需要去鎖定 A 的 mutex,但是他卻已經被 thread 1 鎖定了。
如此一來,就會變成 thread 1 鎖著 A 的 mutex 在等 thread 2 把 B 的 mutex 解鎖,而 thread 2 鎖著 B 的 mutex 在等 thread 1 把 A 的 mutex 解鎖的狀況…這樣的情況,基本上是無解的。
而透過像上面這樣的方法,使用 lock() 這個函式,就可以一口氣把多個 mutex 物件進行鎖定,並免這樣的狀況發生。而實際上,在這種狀況下,使用 unique_lock 也僅只是用來自動管理 mutex 的一種方法,所以其實也還是有其他寫法的~
例如,如果是要使用 lock_guard 的話,就可以寫成(參考):
lock( from.mMutex, to.mMutex ); lock_guard<mutex> lock1( from.mMutex, adopt_lock ); lock_guard<mutex> lock2( to.mMutex, adopt_lock );
這邊的做法,是先透過 lock() 去對兩個帳號的 mutex 物件直接做鎖定的動作,然後再建立 lock_guard 的物件、來做自動釋放的管理。這邊要注意的是,在建立 lock_guard 物件的時候,需要指定第二個參數 adopt_lock,告訴 lock_guard 目前的執行序已經鎖定了這個 mutex,所以不需要再去要求鎖定一次,只要之後自動解除鎖定就可以了。
而如果不想使用 lock_guard 或 unique_lock 來做自動解鎖的話,也可以自己之後手動各自呼叫 mutex::unlock() 來做解除鎖定的動作,也就是:
lock( from.mMutex, to.mMutex ); //...... from.mMutex.unlock(); to.mMutex.unlock();
這篇就先寫到這了。實際上,在 C++11 STL 裡面,還有不少和 thread 相關的東西,像是 call once、atomic、condition variables、futures 等等(參考);不過因為 Heresy 自己也還沒研究完,所以之後有機會研究完之後,再來分享吧~
哦~
沒注意到傳入參數前後相反,謝謝樓主回覆。
讚讚
您好 能再解釋一下 為何要用defer_lock嗎
您說「在 thread 1 裡面已經鎖定了 A 的 mutex,但是在試圖鎖定 B 的 mutex 的時候,thread 2 已經鎖定了 B 的 mutex;而同樣地,這時候 thread 2 也需要去鎖定 A 的 mutex,但是他卻已經被 thread 1 鎖定了。」
但我搞不懂為何thread2會已經鎖定B的mutex,thread2要鎖定B的前提不是一定要拿到A的mutex嗎(因為程式碼中先拿到A再拿B),但此時A顯然在thread1那,因此thread2應該是一直等到A被unlock,才能繼續run下去?
不知道是否有盲點,希望再請樓主解釋一下。
讚讚
t1 是 Account1 轉到 Account2,會先鎖定 Account1 再鎖定 Account2
t2 是 Account2 轉到 Account1,會先鎖定 Account2 再鎖定 Account1
你可以試試把程式碼改成
unique_lock<mutex> lock1( from.mMutex );//, defer_lock );
this_thread::sleep_for(chrono::seconds(1));
unique_lock<mutex> lock2( to.mMutex );//, defer_lock );
讚讚
great
讚讚
你寫得好棒喔~我是新手,一看就懂
什麼時候會出(三)?想看^^
讚讚
其實算是有了?
C++11 Thread 的 condition variable
https://kheresy.wordpress.com/2014/01/09/c11-condition-variable/
C++11 程式的平行化:async 與 future
https://kheresy.wordpress.com/2016/03/14/async-and-future-in-c11/
讚讚
[…] 之前已經有用三篇文章,介紹了 C++ 11 裡、STL 的 Thread 這個函式庫的使用方法了。有興趣的話,請先參考《基本使用》、《多執行序之間的溝通(一)》和《多執行序之間的溝通(二)》這三篇文章。 […]
讚讚
[…] 多執行序之間的溝通(二) […]
讚讚
寫的很棒呢!但是有一處看不懂,“ thread t1( [&](){ transfer( Account1, Account2, 10 ); } ); ” 這句寫法很神奇,能解釋下嘛?尤其是 [&]() 的使用,謝謝!
讚讚
那是 C++11 新的 Lambda expression 的語法
請參考
https://kheresy.wordpress.com/2010/05/27/c0x%ef%bc%9alambda-expression/
讚讚
[…] 多執行序之間的溝通(二) 幫忙推廣一下吧!更多EmailDiggShare on TumblrLike this:喜歡Be the first to like this. […]
讚讚