C++併發實戰9:unique_lock
1 回顧採用RAII手法管理mutex的std::lock_guard其功能是在物件構造時將mutex加鎖,析構時對mutex解鎖,這樣一個棧物件保證了在異常情形下mutex可以在lock_guard物件析構被解鎖,lock_guard擁有mutex的所有權。
explicit lock_guard (mutex_type& m);//必須要傳遞一個mutex作為構造引數 lock_guard (mutex_type& m, adopt_lock_t tag);//tag=adopt_lock表示mutex已經在之前被上鎖,這裡lock_guard將擁有mutex的所有權 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 unique_lock() noexcept; //可以構造一個空的unique_lock物件,此時並不擁有任何mutex 2 explicit unique_lock (mutex_type& m);//擁有mutex,並呼叫mutex.lock()對其上鎖 3 unique_lock (mutex_type& m, try_to_lock_t tag);//tag=try_lock表示呼叫mutex.try_lock()嘗試加鎖 4 unique_lock (mutex_type& m, defer_lock_t tag) noexcept;//tag=defer_lock表示不對mutex加鎖,只管理mutex,此時mutex應該是沒有加鎖的 5 unique_lock (mutex_type& m, adopt_lock_t tag);//tag=adopt_lock表示mutex在此之前已經被上鎖,此時unique_locl管理mutex 6 template <class Rep, class Period> unique_lock (mutex_type& m, const chrono::duration<Rep,Period>& rel_time);//在一段時間rel_time內嘗試對mutex加鎖,mutex.try_lock_for(rel_time) 7 template <class Clock, class Duration> unique_lock (mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);//mutex.try_lock_until(abs_time)直到abs_time嘗試加鎖 8 unique_lock (const unique_lock&) = delete;//禁止拷貝構造 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其它成員函式
~unique_lock();//若unique_lock物件擁有管理的mutex的所有權,mutex沒有被銷燬或者unlock,那麼將執行mutex::unlock()解鎖,並不銷燬mutex物件。
mutex_type* mutex() const noexcept;//返回unique_lock管理的mutex指標,但是unique_lock不會放棄對mutex的管理,若unique_lock對mutex上鎖了,其有義務對mutex解鎖
bool owns_lock() const noexcept;//當mutex被unique_lock上鎖,且mutex沒有解鎖或析構,返回真,否則返回false
explicit operator bool() const noexcept;//同上
4 std::unique_lock增加了靈活性,比如可以對mutex的管理從一個scope通過move語義轉到另一個scope,不像lock_guard只能在一個scope中生存。同時也增加了管理的難度,因此如無必要還是用lock_guard。
5 網上看見一個unique_lock的應用於銀行轉賬的例項,貼在這裡:
#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;//賬戶都有一個鎖mutex
};
void transfer( bank_account &from, bank_account &to, int amount )//這裡缺少一個from==to的條件判斷個人覺得
{
unique_lock<mutex> lock1( from.mMutex, defer_lock );//defer_lock表示延遲加鎖,此處只管理mutex
unique_lock<mutex> lock2( to.mMutex, defer_lock );
lock( lock1, lock2 );//lock一次性鎖住多個mutex防止deadlock
from.iMoney -= amount;
to.iMoney += amount;
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 ); } );//lambda表示式
thread t2( [&](){ transfer( Account2, Account1, 5 ); } );
t1.join();
t2.join();
}
說明:加鎖的時候為什麼不是如下這樣的?在前面一篇博文中有講到多個語句加鎖可能導致deadlock,假設:同一時刻A向B轉賬,B也向A轉賬,那麼先持有自己的鎖再相互請求對方的鎖必然deadlock。
lock_guard<mutex> lock1( from.mMutex );
lock_guard<mutex> lock2( to.mMutex );
採用lock_guard也可以如下:
lock( from.mMutex, to.mMutex );
lock_guard<mutex> lock1( from.mMutex, adopt_lock );//adopt_lock表示mutex已經上鎖,lock1將擁有from.mMutex
lock_guard<mutex> lock2( to.mMutex, adopt_lock );
6 上面的例子lock針對mutex加鎖後,並沒有顯示解鎖,那麼離開lock的作用域後解鎖了嗎?驗證程式碼如下,在lock後丟擲異常mutex解鎖了嗎?:
#include<mutex>
#include<exception>
#include<iostream>
using namespace std;
int main(){
mutex one,two;
try{
{
lock(one,two);
throw 1;
cout<<"locking..."<<endl;
}
}catch(int){
cout<<"catch..."<<endl;
}
if(!one.try_lock()&&!two.try_lock())
cout<<"failure"<<endl;
else
cout<<"success"<<endl;
return 0;
}
程式輸出:
catch...
success //lock後的操作丟擲異常後,mutex解鎖了
7 unique_lock is movable but not copyable.因此可以作為函式返回值,STL容器元素。例如:一個函式採用unique_lock加鎖mutex然後準備好資料並將unique_lock返回給呼叫者,呼叫者在mutex保護下對資料進一步加工,簡單的程式碼如下:
#include<mutex>
#include<iostream>
using namespace std;
mutex m;
unique_lock<mutex> get_lock(){
unique_lock<mutex> lk(m);
cout<<"prepare data..."<<endl;//準備資料
return lk;//移動構造
}
int main(){
unique_lock<mutex> lk(get_lock());
cout<<"process data..."<<endl;//在mutex保護下資料深加工
return 0;
}
8 unique_lock::lock(), unique_lock::unlock()這一組成員函式充分說明了,unique_lock在構造時不必對mutex加鎖且可以在後期某個時候對mutex加鎖; unique_lock可以在自己例項銷燬前呼叫unique_lock::unlock()提前釋放鎖,這對於一些分支語句中可能得到效能提升。