电竞比分网-中国电竞赛事及体育赛事平台

分享

聊聊C++中的Mutex,以及拯救生產(chǎn)力的Boost

 禁忌石 2019-05-31

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

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

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

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

  • C++本身的STL并不是線程安全的。所以缺少了類似與Java并發(fā)庫所提供的一些高性能的線程安全的數(shù)據(jù)結(jié)構(gòu)。(Doug Lea大神親自操刀完成的并發(fā)編程庫,讓JDK5成為Java之中里程碑式的版本)

  • 如果沒有線程安全的數(shù)據(jù)結(jié)構(gòu),退而求其次,可以自己利用互斥量Mutex來實(shí)現(xiàn)。C++的標(biāo)準(zhǔn)庫支持如下的互斥量的實(shí)現(xiàn):

互斥量版本作用
mutexC++11最基本的互斥量
timed_mutexC++11有超時(shí)機(jī)制的互斥量
recursive_mutexC++11可重入的互斥量
recursive_timed_mutexC++11結(jié)合 2,3 特點(diǎn)的互斥量
shared_timed_mutexC++14具有超時(shí)機(jī)制的可共享互斥量
shared_mutexC++17共享的互斥量

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

2.標(biāo)準(zhǔn)庫互斥量的剖析

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

mutex

mutex的中文翻譯就是互斥量,很多人喜歡稱之其為鎖。其實(shí)不是太準(zhǔn)確,因?yàn)槎嗑€程編程本質(zhì)上應(yīng)該通過互斥量之上加鎖,解鎖的操作,來實(shí)現(xiàn)多線程并發(fā)執(zhí)行時(shí)對(duì)互斥資源線程安全的訪問。我們來看看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對(duì)象。當(dāng)存在某線程占有mutex時(shí),所有其他線程若調(diào)用lock則會(huì)阻塞,而調(diào)用try_lockh會(huì)得到false返回值。由上述代碼可以看到,通過mutex加鎖的方式,來確保只有單一線程對(duì)臨界區(qū)的資源進(jìn)行操作。
time_mutex與recursive_mutex的使用也是大同小異,兩者都是基于mutex來實(shí)現(xiàn)的。( 本質(zhì)上是基于recursive_mutex實(shí)現(xiàn)的,mutex為recursive_mutex的特例)
time_mutex則是進(jìn)行加鎖時(shí)可以設(shè)置阻塞的時(shí)間,若超過對(duì)應(yīng)時(shí)長,則返回false。
recursive_mutex則讓單一線程可以多次對(duì)同一互斥量加鎖,同樣,解鎖時(shí)也需要釋放相同多次的鎖。
以上三種類型的互斥量都是包裝了操作系統(tǒng)底層的pthread_mutex_t:
pthread_mutex_t結(jié)構(gòu)

在C++之中并不提倡我們直接對(duì)鎖進(jìn)行操作,因?yàn)樵趌ock之后忘記調(diào)用unlock很容易造成死鎖。而對(duì)臨界資源進(jìn)行操作時(shí),可能會(huì)拋出異常,程序也有可能break,return 甚至 goto,這些情況都極容易導(dǎo)致unlock沒有被調(diào)用。所以C++之中通過RAII來解決這個(gè)問題,它提供了一系列的通用管理互斥量的類:

互斥量管理版本作用
lock_graudC++11基于作用域的互斥量管理
unique_lockC++11更加靈活的互斥量管理
shared_lockC++14共享互斥量的管理
scope_lockC++17多互斥量避免死鎖的管理

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

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)變得更加明晰了,對(duì)于鎖的管理也交給了程序本身來進(jìn)行處理,減少了出錯(cuò)的可能。

shared_mutex

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

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

這里有個(gè)事情挺奇怪的,C++14提供了shared_timed_mutex 而在C++17提供了shared_mutex。其實(shí)shared_timed_mutex涵蓋了shard_mutex的功能。(不知道是不是因?yàn)槊直籨iss了,所以后續(xù)在C++17里將shared_mutex**加了回來)。共享互斥量適用與讀多寫少的場景,舉個(gè)栗子:

long num = 0;std::shared_mutex num_mutex;// 僅有單個(gè)線程可以寫num的值。void numplus() {    std::unique_lock<std::shared_mutex> lock_guard(num_mutex);    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};// 多個(gè)線程同時(shí)讀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的代碼。它可以同時(shí)限制unique_lock與share_lock

不得不說,C++11沒有將共享互斥量集成進(jìn)來,在很多讀多寫少的應(yīng)用場合之中,標(biāo)準(zhǔn)庫本身提供的鎖機(jī)制顯得很雞肋,也從而導(dǎo)致了筆者最終只能求助與boost的解決方案。(其實(shí)也可以通過標(biāo)準(zhǔn)庫的mutex來實(shí)現(xiàn)一個(gè)讀寫鎖,這也是面試筆試之中常常問到的問題。不過太麻煩了,還得考慮和互斥量管理類兼容什么的,果斷放棄啊)

多鎖競爭

還剩下最后一個(gè)要寫的內(nèi)容:scope_lock ,當(dāng)我們要進(jìn)行多個(gè)鎖管理時(shí),很容易出現(xiàn)問題,由于加鎖的先后順序不同導(dǎo)致死鎖。(其實(shí)本來不想寫了,好累。這里就簡單用例子做解釋吧,偷個(gè)懶~~)
如下栗子,加鎖順序不當(dāng)導(dǎo)致死鎖:

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就可以很簡單解決這個(gè)問題了:

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

好吧,媽媽再也不用擔(dān)心我會(huì)死鎖了~~

3.小結(jié)

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

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多