C++11 std::unique

C++多線程編程中通常會對共享的數據進行寫保護,以防止多線程在對共享數據成員進行讀寫時造成資源爭搶導致程序出現未定義的行為。通常的做法是在修改共享數據成員的時候進行加鎖--mutex。在使用鎖的時候通常是在對共享數據進行修改之前進行lock操作,在寫完之後再進行unlock操作,進場會出現由於疏忽導致由於lock之後在離開共享成員操作區域時忘記unlock,導致死鎖。

針對以上的問題,C++11中引入了std::unique_lock與std::lock_guard兩種數據結構。通過對lock和unlock進行一次薄的封裝,實現自動unlock的功能。

 1 std::mutex mut;
2
3 void insert_data()
4 {
5 std::lock_guard<:mutex> lk(mut);
6 queue.push_back(data);
7 }
8
9 void process_data()
10 {
11 std::unqiue_lock<:mutex> lk(mut);
12 queue.pop();
13 }

std::unique_lock 與std::lock_guard都能實現自動加鎖與解鎖功能,但是std::unique_lock要比std::lock_guard更靈活,但是更靈活的代價是佔用空間相對更大一點且相對更慢一點。

1 回顧採用RAII手法管理mutex的std::lock_guard其功能是在對象構造時將mutex加鎖,析構時對mutex解鎖,這樣一個棧對象保證了在異常情形下mutex可以在lock_guard對象析構被解鎖,lock_guard擁有mutex的所有權。

1 explicit lock_guard (mutex_type& m);//必須要傳遞一個mutex作為構造參數
2 lock_guard (mutex_type& m, adopt_lock_t tag);//tag=adopt_lock表示mutex已經在之前被上鎖,這裡lock_guard將擁有mutex的所有權
3 lock_guard (const lock_guard&) = delete;//不允許copy constructor

2 再來看一個與std::lock_guard功能相似但功能更加靈活的管理mutex的對象 std::unique_lock,unique_lock內部持有mutex的狀態:locked,unlocked。unique_lock比lock_guard佔用空間和速度慢一些,因為其要維護mutex的狀態。

 1 1 unique_lock() noexcept; //可以構造一個空的unique_lock對象,此時並不擁有任何mutex
2
3 2 explicit unique_lock (mutex_type& m);//擁有mutex,並調用mutex.lock()對其上鎖
4
5 3 unique_lock (mutex_type& m, try_to_lock_t tag);//tag=try_lock表示調用mutex.try_lock()嘗試加鎖
6
7 4 unique_lock (mutex_type& m, defer_lock_t tag) noexcept;//tag=defer_lock表示不對mutex加鎖,只管理mutex,此時mutex應該是沒有加鎖的
8
9 5 unique_lock (mutex_type& m, adopt_lock_t tag);//tag=adopt_lock表示mutex在此之前已經被上鎖,此時unique_locl管理mutex
10
11 6 template
12 unique_lock (mutex_type& m, const chrono::duration& rel_time);//在一段時間rel_time內嘗試對mutex加鎖,mutex.try_lock_for(rel_time)
13
14 7 template
15 unique_lock (mutex_type& m, const chrono::time_point& abs_time);//mutex.try_lock_until(abs_time)直到abs_time嘗試加鎖
16
17 8 unique_lock (const unique_lock&) = delete;//禁止拷貝構造
18
19 9 unique_lock (unique_lock&& x);//獲得x管理的mutex,此後x不再和mutex相關,x此後相當於一個默認構造的unique_lock,移動構造函數,具備移動語義,movable but not copyable

說明:其中2和5擁有mutex的所有權,而1和4永遠不用有mutex的所有權,3和6及7若嘗試加鎖成功則擁有mutex的所有權

unique_lock 在使用上比lock_guard更具有彈性,和 lock_guard 相比,unique_lock 主要的特色在於:

unique_lock 不一定要擁有 mutex,所以可以透過 default constructor 建立出一個空的 unique_lock。

unique_lock 雖然一樣不可複製(non-copyable),但是它是可以轉移的(movable)。所以,unique_lock 不但可以被函數回傳,也可以放到 STL 的 container 裡。

另外,unique_lock 也有提供 lock()、unlock() 等函數,可以用來加鎖解鎖mutex,也算是功能比較完整的地方。

unique_lock本身還可以用於std::lock參數,因為其具備lock、unlock、try_lock成員函數,這些函數不僅完成針對mutex的操作還要更新mutex的狀態。

3 std::unique_lock其它成員函數

1 ~unique_lock();//若unique_lock對象擁有管理的mutex的所有權,mutex沒有被銷燬或者unlock,那麼將執行mutex::unlock()解鎖,並不銷燬mutex對象。
2 mutex_type* mutex() const noexcept;//返回unique_lock管理的mutex指針,但是unique_lock不會放棄對mutex的管理,若unique_lock對mutex上鎖了,其有義務對mutex解鎖
3 bool owns_lock() const noexcept;//當mutex被unique_lock上鎖,且mutex沒有解鎖或析構,返回真,否則返回false
4 explicit operator bool() const noexcept;//同上

4 std::unique_lock增加了靈活性,比如可以對mutex的管理從一個scope通過move語義轉到另一個scope,不像lock_guard只能在一個scope中生存。同時也增加了管理的難度,因此如無必要還是用lock_guard。

5 網上看見一個unique_lock的應用於銀行轉賬的實例,貼在這裡:

 1 #include 
2 #include
3 #include
4 #include
5 #include
6 using namespace std;
7 struct bank_account//銀行賬戶
8 {
9 explicit bank_account(string name, int money)
10 {
11 sName = name;
12 iMoney = money;
13 }
14
15 string sName;
16 int iMoney;
17 mutex mMutex;//賬戶都有一個鎖mutex
18 };
19 void transfer( bank_account &from, bank_account &to, int amount )//這裡缺少一個from==to的條件判斷個人覺得
20 {
21 unique_lock lock1( from.mMutex, defer_lock );//defer_lock表示延遲加鎖,此處只管理mutex
22 unique_lock lock2( to.mMutex, defer_lock );
23 lock( lock1, lock2 );//lock一次性鎖住多個mutex防止deadlock
24 from.iMoney -= amount;
25 to.iMoney += amount;
26 cout << "Transfer " << amount << " from "<< from.sName << " to " << to.sName << endl;

27 }
28 int main()
29 {
30 bank_account Account1( "User1", 100 );
31 bank_account Account2( "User2", 50 );
32 thread t1( [&](){ transfer( Account1, Account2, 10 ); } );//lambda表達式
33 thread t2( [&](){ transfer( Account2, Account1, 5 ); } );
34 t1.join();
35 t2.join();
36 }

說明:加鎖的時候為什麼不是如下這樣的?在前面一篇博文中有講到多個語句加鎖可能導致deadlock,假設:同一時刻A向B轉賬,B也向A轉賬,那麼先持有自己的鎖再相互請求對方的鎖必然deadlock。

1 lock_guard lock1( from.mMutex );
2 lock_guard lock2( to.mMutex );

採用lock_guard也可以如下:

1 lock( from.mMutex, to.mMutex );
2 lock_guard lock1( from.mMutex, adopt_lock );//adopt_lock表示mutex已經上鎖,lock1將擁有from.mMutex
3 lock_guard lock2( to.mMutex, adopt_lock );

6 上面的例子lock針對mutex加鎖後,並沒有顯示解鎖,那麼離開lock的作用域後解鎖了嗎?驗證代碼如下,在lock後拋出異常mutex解鎖了嗎?:

 1 #include
2 #include
3 #include
4 using namespace std;
5 int main(){
6 mutex one,two;
7 try{
8 {
9 lock(one,two);
10 throw 1;
11 cout<12 }
13 }catch(int){
14 cout<15 }
16 if(!one.try_lock()&&!two.try_lock())
17 cout<18 else
19 cout<20 return 0;
21 }

程序輸出:

catch...

success //lock後的操作拋出異常後,mutex解鎖了

7 unique_lock is movable but not copyable.因此可以作為函數返回值,STL容器元素。例如:一個函數採用unique_lock加鎖mutex然後準備好數據並將unique_lock返回給調用者,調用者在mutex保護下對數據進一步加工,簡單的代碼如下:

 1 #include
2 #include
3 using namespace std;
4 mutex m;
5 unique_lock get_lock(){

6 unique_lock lk(m);
7 cout< 8 return lk;//移動構造
9 }
10 int main(){
11 unique_lock lk(get_lock());
12 cout<13 return 0;
14 }

8 unique_lock::lock(), unique_lock::unlock()這一組成員函數充分說明了,unique_lock在構造時不必對mutex加鎖且可以在後期某個時候對mutex加鎖; unique_lock可以在自己實例銷燬前調用unique_lock::unlock()提前釋放鎖,這對於一些分支語句中可能得到性能提升。

C++11 std::unique_lock與std::lock_guard區別及多線程應用實例


分享到:


相關文章: