C++霧中風景12:聊聊C++中的Mutex蛉艾,以及拯救生產(chǎn)力的Boost

筆者近期在工作之中編程實現(xiàn)一個Cache結(jié)構(gòu)的封裝,需要使用到C++之中的互斥量Mutex晤愧,于是花了一些時間進行了調(diào)研大莫。(結(jié)果對C++標準庫很是絕望....)最終還是通過利用了Boost庫的shared_mutex解決了問題。借這個機會來聊聊在C++之中的多線程編程的一些“坑”官份。

1.C++多線程編程的困擾

C++從11開始在標準庫之中引入了線程庫來進行多線程編程只厘,在之前的版本需要依托操作系統(tǒng)本身提供的線程庫來進行多線程的編程。(其實本身就是在標準庫之上對底層的操作系統(tǒng)多線程API統(tǒng)一進行了封裝舅巷,筆者本科時進行操作系統(tǒng)實驗是就是使用的pthread或<windows.h>來進行多線程編程的
提供了統(tǒng)一的多線程固然是好事羔味,但是標準庫給的支持實在是有限,具體實踐起來還是讓人挺困擾的:

  • C++本身的STL并不是線程安全的钠右。所以缺少了類似與Java并發(fā)庫所提供的一些高性能的線程安全的數(shù)據(jù)結(jié)構(gòu)赋元。(Doug Lea大神親自操刀完成的并發(fā)編程庫,讓JDK5成為Java之中里程碑式的版本)
  • 如果沒有線程安全的數(shù)據(jù)結(jié)構(gòu),退而求其次搁凸,可以自己利用互斥量Mutex來實現(xiàn)媚值。C++的標準庫支持如下的互斥量的實現(xiàn):
互斥量 版本 作用
mutex C++11 最基本的互斥量
timed_mutex C++11 有超時機制的互斥量
recursive_mutex C++11 可重入的互斥量
recursive_timed_mutex C++11 結(jié)合 2,3 特點的互斥量
shared_timed_mutex C++14 具有超時機制的可共享互斥量
shared_mutex C++17 共享的互斥量

由上述表格可見,C++是從14之后的版本才正式支持共享互斥量护糖,也就是實現(xiàn)讀寫鎖的結(jié)構(gòu)褥芒。由于筆者的公司僅支持C++11的版本,所以就沒有辦法使用共享互斥量來實現(xiàn)讀寫鎖了嫡良。所以最終筆者只好求助與boost的庫锰扶,利用boost提供的讀寫鎖來完成了所需完成的工作。(所以對工具不足時可以考慮求助于boost庫皆刺,確實是解放生產(chǎn)力的大殺器少辣,C++的標準庫實在太簡陋了~~)

2.標準庫互斥量的剖析

雖然吐槽了一小節(jié),但并不影響繼續(xù)去學習C++標準庫給我們提供的工具.........(但愿公司能再推動升級一波C++的版本~~不過看起來是遙遙無期了)接下來筆者就要來帶領(lǐng)大家簡單剖析一些C++標準庫之中互斥量羡蛾。

mutex

mutex的中文翻譯就是互斥量,很多人喜歡稱之其為鎖锨亏。其實不是太準確痴怨,因為多線程編程本質(zhì)上應該通過互斥量之上加鎖,解鎖的操作器予,來實現(xiàn)多線程并發(fā)執(zhí)行時對互斥資源線程安全的訪問浪藻。 我們來看看mutex類的使用方法:

long num = 0;
std::mutex num_mutex;

void numplus() {
    num_mutex.lock();
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
    num_mutex.unlock();
};

void numsub() {
    num_mutex.lock();
    for (long i = 0; i < 1000000; ++i) {
        num--;
    }
    num_mutex.unlock();
}

int main() {
    std::thread t1(numplus);
    std::thread t2(numsub);
    t1.join();
    t2.join();
    std::cout << num << std::endl;
}

調(diào)用線程從成功調(diào)用lock()或try_lock()開始,到unlock()為止占有mutex對象乾翔。當存在某線程占有mutex時爱葵,所有其他線程若調(diào)用lock則會阻塞,而調(diào)用try_lockh會得到false返回值反浓。由上述代碼可以看到萌丈,通過mutex加鎖的方式,來確保只有單一線程對臨界區(qū)的資源進行操作雷则。
time_mutex與recursive_mutex的使用也是大同小異辆雾,兩者都是基于mutex來實現(xiàn)的。( 本質(zhì)上是基于recursive_mutex實現(xiàn)的月劈,mutex為recursive_mutex的特例)
time_mutex則是進行加鎖時可以設(shè)置阻塞的時間度迂,若超過對應時長,則返回false猜揪。
recursive_mutex則讓單一線程可以多次對同一互斥量加鎖惭墓,同樣,解鎖時也需要釋放相同多次的鎖而姐。
以上三種類型的互斥量都是包裝了操作系統(tǒng)底層的pthread_mutex_t:

pthread_mutex_t結(jié)構(gòu)

在C++之中并不提倡我們直接對鎖進行操作腊凶,因為在lock之后忘記調(diào)用unlock很容易造成死鎖。而對臨界資源進行操作時,可能會拋出異常吭狡,程序也有可能break尖殃,return 甚至 goto,這些情況都極容易導致unlock沒有被調(diào)用划煮。所以C++之中通過RAII來解決這個問題送丰,它提供了一系列的通用管理互斥量的類:

互斥量管理 版本 作用
lock_graud C++11 基于作用域的互斥量管理
unique_lock C++11 更加靈活的互斥量管理
shared_lock C++14 共享互斥量的管理
scope_lock C++17 多互斥量避免死鎖的管理

創(chuàng)建互斥量管理對象時,它試圖給給定mutex加鎖弛秋。當程序離開互斥量管理對象的作用域時器躏,互斥量管理對象會析構(gòu)并且并釋放mutex。所以我們則不需要擔心程序跳出或產(chǎn)生異常引發(fā)的死鎖了蟹略。
對于需要加鎖的代碼段登失,可以通過{}括起來形成一個作用域。比如上述代碼的栗子挖炬,可以進行如下改寫(推薦):

long num = 0;
std::mutex num_mutex;

void numplus() {
    std::lock_guard<std::mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};
void numsub() {
    std::lock_guard<std::mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num--;
    }
}

int main() {
    std::thread t1(numplus);
    std::thread t2(numsub);
    t1.join();
    t2.join();
    std::cout << num << std::endl;
}

由上述代碼可以看到揽浙,代碼結(jié)構(gòu)變得更加明晰了,對于鎖的管理也交給了程序本身來進行處理意敛,減少了出錯的可能馅巷。

shared_mutex

C++14的版本之后提供了共享互斥量,它的區(qū)別就在于提供更加細粒度的加鎖操作:lock_shared草姻。lock_shared是一個獲取共享鎖的操作钓猬,而lock是一個獲取排他鎖的操作,通過這種方式更加細粒度化鎖的操作撩独。shared_mutex也是基于操作系統(tǒng)底層的讀寫鎖pthread_rwlock_t的封裝:

pthread_rwlock_t的結(jié)構(gòu)

這里有個事情挺奇怪的敞曹,C++14提供了shared_timed_mutex 而在C++17提供了shared_mutex。其實shared_timed_mutex涵蓋了shard_mutex的功能综膀。(不知道是不是因為名字被diss了澳迫,所以后續(xù)在C++17里將shared_mutex**加了回來)。共享互斥量適用與讀多寫少的場景僧须,舉個栗子:

long num = 0;
std::shared_mutex num_mutex;

// 僅有單個線程可以寫num的值纲刀。
void numplus() {
    std::unique_lock<std::shared_mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};

// 多個線程同時讀num的值。
long numprint() {
    std::shared_lock<std::shared_mutex> lock_guard(num_mutex);
    return num;
}

簡單來說:

  • shared_lock是讀鎖担平。被鎖后仍允許其他線程執(zhí)行同樣被shared_lock的代碼
  • unique_lock是寫鎖示绊。被鎖后不允許其他線程執(zhí)行被shared_lock或unique_lock的代碼。它可以同時限制unique_lock與share_lock

不得不說暂论,C++11沒有將共享互斥量集成進來面褐,在很多讀多寫少的應用場合之中,標準庫本身提供的鎖機制顯得很雞肋取胎,也從而導致了筆者最終只能求助與boost的解決方案展哭。(其實也可以通過標準庫的mutex來實現(xiàn)一個讀寫鎖湃窍,這也是面試筆試之中常常問到的問題。不過太麻煩了匪傍,還得考慮和互斥量管理類兼容什么的您市,果斷放棄啊)

多鎖競爭

還剩下最后一個要寫的內(nèi)容:scope_lock ,當我們要進行多個鎖管理時役衡,很容易出現(xiàn)問題茵休,由于加鎖的先后順序不同導致死鎖。(其實本來不想寫了手蝎,好累榕莺。這里就簡單用例子做解釋吧,偷個懶~~)
如下栗子棵介,加鎖順序不當導致死鎖:

std::mutex m1, m2;
// thread 1
{
  std::lock_guard<std::mutex> lock1(m1);
  std::lock_guard<std::mutex> lock2(m2);
}
// thread 2
{
  std::lock_guard<std::mutex> lock2(m2);
  std::lock_guard<std::mutex> lock1(m1);
}

而通過C++17提供的scope_lock就可以很簡單解決這個問題了:

std::mutex m1, m2;
// thread 1
{
  std::scope_lock lock(m1, m2);
}
// thread 2
{
  std::scope_lock lock(m1, m2);
}

好吧钉鸯,媽媽再也不用擔心我會死鎖了~~

3.小結(jié)

算是簡單的梳理完C++標準庫之中的mutex了,也通過一些栗子比較完整的展現(xiàn)了使用方式邮辽。筆者上述關(guān)于標準庫的內(nèi)容唠雕,在boost庫之中都能找到對應的實現(xiàn),不過如果能夠使用標準庫逆巍,盡量還是不要引用boost了及塘。(走投無路的時候記得求助boost,真香~~)希望大家在實踐之中可以很好的運用好這些C++互斥量來更好的確保線程安全了锐极。后續(xù)筆者還會繼續(xù)深入的探討有關(guān)C++多線程的相關(guān)內(nèi)容,歡迎大家多多指教芳肌。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灵再,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子亿笤,更是在濱河造成了極大的恐慌翎迁,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件净薛,死亡現(xiàn)場離奇詭異汪榔,居然都是意外死亡,警方通過查閱死者的電腦和手機肃拜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門痴腌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人燃领,你說我怎么就攤上這事士聪。” “怎么了猛蔽?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵剥悟,是天一觀的道長灵寺。 經(jīng)常有香客問我,道長区岗,這世上最難降的妖魔是什么略板? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮慈缔,結(jié)果婚禮上叮称,老公的妹妹穿的比我還像新娘。我一直安慰自己胀糜,他們只是感情好颅拦,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著教藻,像睡著了一般距帅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上括堤,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天碌秸,我揣著相機與錄音,去河邊找鬼悄窃。 笑死讥电,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的轧抗。 我是一名探鬼主播恩敌,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼横媚!你這毒婦竟也來了纠炮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤灯蝴,失蹤者是張志新(化名)和其女友劉穎恢口,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體穷躁,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡耕肩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了问潭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猿诸。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖睦授,靈堂內(nèi)的尸體忽然破棺而出两芳,到底是詐尸還是另有隱情,我是刑警寧澤去枷,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布怖辆,位于F島的核電站是复,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏竖螃。R本人自食惡果不足惜淑廊,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望特咆。 院中可真熱鬧季惩,春花似錦、人聲如沸腻格。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽菜职。三九已至青抛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間酬核,已是汗流浹背蜜另。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫡意,地道東北人举瑰。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像蔬螟,于是被迫代替她去往敵國和親此迅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

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