C++線程間共享數(shù)據(jù)

通常我們使用鎖保護線程間共享數(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_mutexshared_mutex

2.1 recursive_mutex

recursive_mutexmutex行為幾乎一致阱高,區(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_lockstd::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_flagstd::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();
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痰洒,一起剝皮案震驚了整個濱河市瓢棒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丘喻,老刑警劉巖脯宿,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泉粉,居然都是意外死亡连霉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窘面,“玉大人翠语,你說我怎么就攤上這事〔票撸” “怎么了肌括?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長酣难。 經(jīng)常有香客問我谍夭,道長,這世上最難降的妖魔是什么憨募? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任紧索,我火速辦了婚禮,結(jié)果婚禮上菜谣,老公的妹妹穿的比我還像新娘珠漂。我一直安慰自己,他們只是感情好尾膊,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布媳危。 她就那樣靜靜地躺著,像睡著了一般冈敛。 火紅的嫁衣襯著肌膚如雪待笑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天抓谴,我揣著相機與錄音暮蹂,去河邊找鬼。 笑死癌压,一個胖子當(dāng)著我的面吹牛仰泻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播滩届,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼我纪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了丐吓?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤趟据,失蹤者是張志新(化名)和其女友劉穎券犁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汹碱,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡粘衬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稚新。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡勘伺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出褂删,到底是詐尸還是另有隱情飞醉,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布屯阀,位于F島的核電站缅帘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏难衰。R本人自食惡果不足惜钦无,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盖袭。 院中可真熱鬧失暂,春花似錦、人聲如沸鳄虱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽醇蝴。三九已至宣肚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悠栓,已是汗流浹背霉涨。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惭适,地道東北人笙瑟。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像癞志,于是被迫代替她去往敵國和親往枷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1 概述 2 管理線程: thread/join/detach/RAAI/std::ref/std::bind/m...
    my_passion閱讀 904評論 0 0
  • 參考cplusplus參考cppreference 1.mutex 用于保護臨界區(qū)(critical sectio...
    王偵閱讀 4,270評論 0 0
  • mutex又稱互斥量凄杯,用于提供對共享變量的互斥訪問错洁。C++11中mutex相關(guān)的類都在<mutex>頭文件中。共四...
    許了閱讀 9,890評論 3 5
  • lock 類 std::lock_guard戒突,與 mutex RAII 相關(guān)屯碴,方便線程對互斥量上鎖。std::un...
    鐘離惜閱讀 1,314評論 0 0
  • 最近是恰好寫了一些c++11多線程有關(guān)的東西膊存,就寫一下筆記留著以后自己忘記回來看吧导而,也不是專門寫給讀者看的忱叭,我就想...
    編程小世界閱讀 2,501評論 1 2