通常我們使用鎖保護線程間共享數(shù)據(jù)庇谆,這也是最基本的方式缀台。
當(dāng)訪問共享數(shù)據(jù)前棠赛,使用互斥量將相關(guān)數(shù)據(jù)鎖住,再當(dāng)訪問結(jié)束后将硝,再將數(shù)據(jù)解鎖恭朗。線程庫需要保證,當(dāng)一個線程使用特定互斥量鎖住共享數(shù)據(jù)時依疼,其他的線程想要訪問鎖住的數(shù)據(jù),都必須等到之前那個線程對數(shù)據(jù)進行解鎖后而芥,才能進行訪問律罢。這就保證了所有線程能看到共享數(shù)據(jù),而不破壞不變量棍丐。
1 使用互斥量
C++提供 std::mutex
創(chuàng)建互斥量误辑,通過調(diào)用 lock()
上鎖,unlock()
解鎖歌逢。
為方便使用巾钉,C++提供RAII語法的模板類 std::lock_guard()
,可以在離開鎖作用域是自動解鎖秘案。其有以下特點:
- 創(chuàng)建即加鎖砰苍,作用域結(jié)束自動析構(gòu)并解鎖,無需手動解鎖
- 不能中途解鎖
- 不能復(fù)制
show me the case
int g_i = 0;
std::mutex g_i_mutex;
void safe_increment() {
std::lock_guard lock(g_i_mutex); //safe_increment結(jié)束時自動解鎖
g_i++;
}
2 其他類型互斥量
接下來介紹另外兩種互斥量: std::recursive_mutex
和 shared_mutex
2.1 recursive_mutex
recursive_mutex
和 mutex
行為幾乎一致阱高,區(qū)別在于提供排他性遞歸所有權(quán)語義赚导,已經(jīng)獲得一個遞歸互斥體的所有權(quán)的線程允許在同一個互斥體上再次調(diào)用 lock()
和 try_lock()
, 調(diào)用線程調(diào)用 unlock
的次數(shù)應(yīng)該等于獲的這個遞歸互斥鎖的次數(shù),在匹配次數(shù)時解鎖赤惊。
比如函數(shù)A需要獲取鎖mutex吼旧,函數(shù)B也需要獲取鎖mutex,同時函數(shù)A中還會調(diào)用函數(shù)B未舟。如果使用 std::mutex
必然會造成死鎖圈暗。但是使用 std::recursive_mutex
就可以解決這個問題
2.2 shared_mutex(C++ 17)
shared_mutex
類是一個同步原語掂为,可用于保護共享數(shù)據(jù)不被多個線程同時訪問。與便于獨占訪問的其他互斥類型不同员串,shared_mutex
擁有二個訪問級別:
- 共享 - 多個線程能共享同一互斥的所有權(quán)勇哗。
- 獨占性 - 僅一個線程能占有互斥。
若一個線程已獲取獨占性鎖(通過 lock 昵济、 try_lock )智绸,則無其他線程能獲取該鎖(包括共享的)。僅當(dāng)任何線程均未獲取獨占性鎖時访忿,共享鎖能被多個線程獲惹评酢(通過 lock_shared 、 try_lock_shared )海铆。在一個線程內(nèi)迹恐,同一時刻只能獲取一個鎖(共享或獨占性)。共享互斥體在能由任何數(shù)量的線程同時讀共享數(shù)據(jù)卧斟,但一個線程只能在無其他線程同時讀寫時寫同一數(shù)據(jù)時特別有用殴边。
- 排他性鎖定
- lock(): 鎖定互斥,若互斥則阻塞
- try_lock(): 嘗試鎖定互斥珍语,若互斥不可用則返回
- unlock(): 解鎖互斥
- 共享鎖定
- lock_shared():為共享所有權(quán)鎖定互斥锤岸,若互斥不可用則阻塞
- try_lock_shared(): 嘗試為共享所有權(quán)鎖定互斥,若互斥不可用則返回
- unlock_shard(): 解鎖互斥
3 各種鎖介紹
前面已經(jīng)簡單介紹了 std::lock_guard
板乙,接下來將介紹其他類型的常用鎖是偷。
3.1 std::unique_lock -- 更加靈活的鎖
std::unique_lock
比 std::lock_guard
靈活很多,效率上差一點募逞,內(nèi)存占用多一點蛋铆,可移動,但不可復(fù)制放接。
std::unique_lock
構(gòu)造時除了接受第一個參數(shù)mlock, 可接受第二個參數(shù)刺啦,指定鎖定策略:
- defer_lock_t:不獲得互斥的所有權(quán),即僅僅構(gòu)造unique_lock與mlock關(guān)聯(lián)纠脾,但是并不上鎖
- try_to_lock_t:嘗試獲得互斥的所有權(quán)而不阻塞玛瘸,可以使用成員函數(shù)
bool owns_lock()
檢測是否上鎖成功 - adopt_lock_t:假設(shè)調(diào)用方線程已擁有互斥的所有權(quán),即構(gòu)造構(gòu)造unique_lock與mlock關(guān)聯(lián)之前乳乌,已經(jīng)對mlock加鎖
show me the case
//defer_lock_t
void transfer(bank_account &from, bank_account &to, int amount)
{
// 鎖定兩個互斥而不死鎖
std::lock(from.m, to.m); //對from.m和to.m加鎖
// 保證二個已鎖定互斥在作用域結(jié)尾解鎖
std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);
from.balance -= amount;
to.balance += amount;
}
//try_to_lock_t
for (int i = 1; i <= 5000; i++) {
std::unique_lock<std::mutex> munique(mlock, std::try_to_lock);
if (munique.owns_lock() == true) { //加鎖成功
s += i;
}
else {
// 執(zhí)行一些沒有共享內(nèi)存的代碼
}
//adopt_lock_t
void transfer(bank_account &from, bank_account &to, int amount)
{
// 保證二個已鎖定互斥在作用域結(jié)尾解鎖
std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
std::lock(lock1, lock2); //對互斥量加鎖
from.balance -= amount;
to.balance += amount;
}
不同域中互斥量所有權(quán)的傳遞
std::unique_lock
實例沒有與自身相關(guān)的互斥量捧韵,一個互斥量的所有權(quán)可以通過移動操作,在不同的實例中進行傳遞汉操。某些情況下再来,這種轉(zhuǎn)移是自動發(fā)生的,例如:當(dāng)函數(shù)返回一個實例;另些情況下芒篷,需要顯式的調(diào)用 std::move()
來執(zhí)行移動操作搜变。
std::unique_lock<std::mutex> get_lock()
{
extern std::mutex some_mutex;
std::unique_lock<std::mutex> lk(some_mutex); //構(gòu)造unique_lock
prepare_data();
return lk; //返回unique_lock的指針,離開作用域lk不會被銷毀针炉,而是move到域外
}
void process_data()
{
std::unique_lock<std::mutex> lk(get_lock()); // 獲得鎖所有權(quán)
do_something();
}
3.2 單次調(diào)用加鎖
std::once_flag
是 std::call_once
的輔助類挠他。傳遞給多個 std::call_once 調(diào)用的 std::once_flag 對象允許那些調(diào)用彼此協(xié)調(diào),從而只令調(diào)用之一實際運行完成篡帕。
std::call_once
準(zhǔn)確執(zhí)行一次可調(diào)用 (Callable) 對象 f 殖侵,即使同時從多個線程調(diào)用。
- 若在調(diào)用 call_once 的時刻镰烧, flag 指示已經(jīng)調(diào)用了 f 拢军,則 call_once 立即返回(稱這種對 call_once 的調(diào)用為消極)
- 否則調(diào)用可調(diào)用 (Callable) 對象f執(zhí)行
- 若該調(diào)用拋異常,則傳播異常給 call_once 的調(diào)用方怔鳖,并且不翻轉(zhuǎn) flag 茉唉,以令其他調(diào)用將得到嘗試(稱這種對 call_once 的調(diào)用為異常)。
- 若該調(diào)用正常返回(稱這種對 call_once 的調(diào)用為返回)结执,則翻轉(zhuǎn) flag 度陆,并保證以同一 flag 對 call_once 的其他調(diào)用為消極。
std::once_flag flag1, flag2;
void simple_do_once()
{
std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
}
void may_throw_function(bool do_throw)
{
if (do_throw) {
// 這會出現(xiàn)多于一次, 因為出現(xiàn)異常的話献幔,其他的調(diào)用會得到嘗試
std::cout << "throw: call_once will retry\n";
throw std::exception();
}
// 如果未發(fā)生異常懂傀,則保證函數(shù)只會被調(diào)用一次
std::cout << "Didn't throw, call_once will not attempt again\n";
}
void do_once(bool do_throw)
{
try {
std::call_once(flag2, may_throw_function, do_throw);
}
catch (...) {
}
}
int main()
{
std::thread st1(simple_do_once); // 1
std::thread st2(simple_do_once); // 2
std::thread st3(simple_do_once); // 3
std::thread st4(simple_do_once); // 4
st1.join();
st2.join();
st3.join();
st4.join();
std::thread t1(do_once, true); //5
std::thread t2(do_once, true); //6
std::thread t3(do_once, false); //7
std::thread t4(do_once, true); //8
t1.join();
t2.join();
t3.join();
t4.join();
}
可能的結(jié)果:
Simple example:
//1、2蜡感、3鸿竖、4只會成功調(diào)用一次
called once
//5、6铸敏、8會觸發(fā)異常,現(xiàn)在的結(jié)果可能是5悟泵、6觸發(fā)兩次異常杈笔,7調(diào)用成功,8不會再觸發(fā)調(diào)用而是立刻返回
throw: call_once will retry
throw: call_once will retry
Didn't throw, call_once will not attempt again
3.3 同時加鎖多個互斥量
3.3.1 std::lock
鎖定給定的可鎖定 (Lockable) 對象 lock1 糕非、 lock2 蒙具、 ... 、 lockn 朽肥,用免死鎖算法避免死鎖禁筏。以對 lock 、 try_lock 和 unlock 的未指定系列調(diào)用鎖定對象衡招。若調(diào)用 lock 或 unlock 導(dǎo)致異常篱昔,則在重拋前對任何已鎖的對象調(diào)用 unlock。
std::lock
鎖住的鎖不會自動釋放鎖,需要手動解鎖州刽, 因此 std::lock
常與 std::lock_guard
或者 std::unique_lock
結(jié)合使用空执,比如
std::lock(m1, m2); //此處加鎖,構(gòu)造的lock_guard無需上鎖穗椅,離開作用域自動解鎖
std::lock_guard lock1(m1, std::adopt_lock);
std::lock_guard lock2(m2, std::adopt_lock);
或者
std::unique_lock lock1(m1, std::defer_lock);
std::unique_lock lock2(m2, std::defer_lock);
std::lock(lock1, lock2) //構(gòu)造的unique_lock未上鎖辨绊,此處加鎖,離開作用域自動解鎖
std::try_lock
的作用是與 std::lock
相似匹表,可以同時對多個互斥量加鎖而不會死鎖门坷,通過以從頭開始的順序調(diào)用 try_lock 。
若調(diào)用 try_lock 失敗袍镀,則不再進一步調(diào)用 try_lock 默蚌,并對任何已鎖對象調(diào)用 unlock ,返回鎖定失敗對象的 0 底下標(biāo)流椒。成功時為 -1 敏簿,否則為鎖定失敗對象的 0 底下標(biāo)值。
若調(diào)用 try_lock 拋出異常宣虾,則在重拋前對任何已鎖對象調(diào)用 unlock 惯裕。
3.3.2 std::scoped_lock(C++ 17)
類 scoped_lock 是提供便利 RAII 風(fēng)格機制的互斥包裝器,它在作用域塊的存在期間占有一或多個互斥绣硝。
創(chuàng)建 scoped_lock 對象時蜻势,它試圖取得給定互斥的所有權(quán)○呐郑控制離開創(chuàng)建 scoped_lock 對象的作用域時握玛,析構(gòu) scoped_lock 并釋放互斥。若給出數(shù)個互斥甫菠,則使用免死鎖算法挠铲,如同以 std::lock 。
show me the case
std::scoped_lock lock(m1, m2);
//等價代碼1
std::lock(m1, m2);
std::lock_guard<std::mutex> lk1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lk2(m2, std::adopt_lock);
//等價代碼2
std::unique_lock<std::mutex> lk1(m1, std::defer_lock);
std::unique_lock<std::mutex> lk2(m2, std::defer_lock);
std::lock(lk1, lk2);
3.4 共享鎖(C++ 14)
std::shared_lock
會以共享模式鎖定關(guān)聯(lián)的共享互斥(std::unique_lock
可用于以排他性模式鎖定)寂诱。
class SaferCounter {
std::shared_mutex mutex;
unsigned int get() const {
std::shared_lock<std::shared_mutex> lock(mutex);//獲取共享鎖拂苹,內(nèi)部執(zhí)行mutex.lock_shared()
return value_; //lock 析構(gòu), 執(zhí)行mutex.unlock_shared();
}
unsigned int increment() {
std::unique_lock<std::shared_mutex> lock(mutex) //獲取獨占鎖,內(nèi)部執(zhí)行mutex.lock()
value++;
return value; //lock 析構(gòu), 執(zhí)行mutex.unlock();
}
}