目錄
前言
一、基本鎖
1. 互斥鎖(mutex)
2. 定時(shí)互斥鎖(timed_mutex)
3. 條件變量 (condition_variable)
4. 讀寫鎖 (shared_mutex)
5. 遞歸鎖(recursive_mutex)
6. 自旋鎖 (spinlock)
二、RAII鎖
1. lock_guard
2. unique_lock
3. shared_lock
三、信號(hào)量
總結(jié)
前言
多線程編程一個(gè)重要的問(wèn)題就是數(shù)據(jù)競(jìng)爭(zhēng),多個(gè)線程同時(shí)獲取一份數(shù)據(jù)的使用權(quán),如果不加以控制,必然會(huì)導(dǎo)致程序的崩潰。
鎖(mutex),就是用來(lái)調(diào)度各線程使用共享數(shù)據(jù)的中介。鎖有已鎖和未鎖兩個(gè)狀態(tài),處于未鎖狀態(tài)的鎖,線程可以將它鎖上,此時(shí)只能由該線程訪問(wèn)共享數(shù)據(jù);如果鎖處于已鎖狀態(tài),則需要等待別的線程解鎖之后,才能上鎖并使用數(shù)據(jù),這樣就避免了多個(gè)線程同時(shí)訪問(wèn)一份數(shù)據(jù)。如果不好理解,可以類比以下廁所的一個(gè)坑位。。。
本篇文章主要對(duì)C++現(xiàn)有的鎖進(jìn)行介紹,由于鎖的種類繁多,而且相關(guān)文章已經(jīng)非常多了,本文不再細(xì)講各種鎖的技術(shù)細(xì)節(jié),而是更注重于各種鎖的由來(lái),以及他們針對(duì)的問(wèn)題,如此便能針對(duì)自己面臨的問(wèn)題,選擇合適的鎖
。
今天是2023年4月21號(hào),目前我使用的是C++20,因此在這篇文章,我們記錄C++20中各類的鎖。主要包括基本鎖和RAII鎖,基本鎖包括互斥鎖 (mutex),定時(shí)互斥鎖 (timed_mutex),條件變量 (condition_variable),讀寫鎖 (shared_mutex),遞歸鎖 (recursive_mutex),自旋鎖 (spinlock)。RAII鎖是基本基本鎖實(shí)現(xiàn)的更加智能的鎖。
一、基本鎖
1. 互斥鎖(mutex)
起始版本 | C++11 |
頭文件 | <mutex > |
接口 |
鎖定互斥鎖,若另一線程已鎖定互斥鎖,則到 |
嘗試鎖定互斥鎖,成功上鎖返回 | |
解鎖互斥鎖。 |
?描述:互斥鎖是最基本、簡(jiǎn)單的鎖,只有三個(gè)接口,lock
()上鎖,unlock
()解鎖,try_lock
()嘗試上鎖。lock
()只會(huì)進(jìn)行一次上鎖操作,如果失敗了(其他線程正在占用),就會(huì)進(jìn)入睡眠狀態(tài)并阻塞線程,等到互斥鎖解鎖之后,系統(tǒng)會(huì)將其喚醒并進(jìn)行上鎖操作(當(dāng)然,這時(shí)候也不一定能成功)。try_lock
()上鎖失敗會(huì)返回false
,并繼續(xù)執(zhí)行后面的代碼。
針對(duì)問(wèn)題:為了初步解決多線程之間數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題。
使用場(chǎng)景:由于是最基本的鎖,在一些復(fù)雜場(chǎng)景中難以滿足需求,因此多用于一些不算復(fù)雜的情況。
2. 定時(shí)互斥鎖(timed_mutex)
起始版本 | C++11 |
頭文件 | <mutex > |
接口 |
鎖定互斥鎖,若另一線程已鎖定互斥鎖,則到 |
嘗試鎖定互斥鎖,成功上鎖返回true,若另一線程已鎖定互斥鎖,則返回false,不會(huì)阻塞 | |
解鎖互斥鎖。 | |
template< class Rep, class Period > 嘗試加鎖,如果鎖被占用,阻塞最久 | |
template< class Clock, class Duration > 嘗試獲取互斥鎖。阻塞直至抵達(dá)指定的 |
?描述:相對(duì)于mutex
而言,timed_mutex
增加了try_lock_for
和try_lock_until
兩個(gè)接口,前者在嘗試獲取鎖失敗時(shí)會(huì)等待指定時(shí)間,在此期間會(huì)不斷嘗試獲取鎖;后者會(huì)獲取鎖失敗時(shí)會(huì)等待至指定時(shí)刻。如果忽視這兩個(gè)接口,那么timed_mutex
與mutex
無(wú)異。
針對(duì)問(wèn)題:為了緩解mutex
不夠靈活的問(wèn)題,增加了可選的等待時(shí)延。
3. 條件變量 (condition_variable)
起始版本 | C++11 |
頭文件 | <condition_variable > |
接口 |
等待并阻塞線程,直到別的線程進(jìn)行通知,wait( )會(huì)釋放lock。 |
template< class Predicate > pred是一個(gè)lambda函數(shù),返回值為bool,此時(shí)的wait等價(jià)于 while (!pred()) { wait(lock); } | |
最多等待 | |
最多等待至 | |
通知一個(gè)正在 | |
通知所有正在 |
描述:條件變量的作用是用于多線程之間的線程同步。線程同步是指線程間需要按照預(yù)定的先后順序進(jìn)行的行為,比如我想要線程1完成了某個(gè)步驟之后,才允許線程2開(kāi)始工作,這個(gè)時(shí)候就可以使用條件變量來(lái)達(dá)到目的。具體操作就可以是,線程2調(diào)用wait
函數(shù)進(jìn)行等待,線程1調(diào)用notify
函數(shù)進(jìn)行通知,這樣就能保證線程1和線程2的順序。
針對(duì)問(wèn)題:上邊說(shuō)的線程同步問(wèn)題,用互斥鎖mutex
也能實(shí)現(xiàn),但是并不優(yōu)美。這里我引用別人的一個(gè)解釋:
以一個(gè)生產(chǎn)者消費(fèi)者的例子來(lái)看,生產(chǎn)者和消費(fèi)者通過(guò)一個(gè)隊(duì)列連接,因?yàn)殛?duì)列屬于共享變量,所以在訪問(wèn)隊(duì)列時(shí)需要加鎖。生產(chǎn)者向隊(duì)列中放入消息的時(shí)間是不一定的,因?yàn)橄M(fèi)者不知道隊(duì)列什么時(shí)候有消息,所以只能不停循環(huán)判斷或者sleep一段時(shí)間,不停循環(huán)會(huì)浪費(fèi)cpu資源,如果sleep那么要sleep多久,sleep太短又會(huì)浪費(fèi)資源,sleep太長(zhǎng)又會(huì)導(dǎo)致消息消費(fèi)不及時(shí)。
應(yīng)用場(chǎng)景:多用于生產(chǎn)消費(fèi)隊(duì)列中。
相關(guān)文章:
?C++11條件變量condition_variable詳解
為什么互斥鎖和條件變量要一起使用
4. 讀寫鎖 (shared_mutex)
起始版本 | C++17 |
頭文件 | <shared_mutex > |
接口 |
排他式鎖定互斥。若另一線程已鎖定互斥,則到 |
嘗試排他式鎖定互斥。立即返回,成功獲得鎖時(shí)返回 | |
解鎖排他式互斥。 | |
以共享式鎖定互斥。如果另一線程以排他式鎖定互斥,則會(huì)阻塞,直到獲得鎖;如果另一線程或者多個(gè)線程以共享式鎖定了互斥,則調(diào)用者同樣會(huì)獲得鎖。 | |
嘗試共享式鎖定互斥。立即返回,成功獲得鎖時(shí)返回 | |
解鎖共享互斥。 |
描述:shared_mutex有兩種上鎖方式,一種是排他式,另一種是共享式。排他式上鎖同一時(shí)間只允許一個(gè)線程擁有鎖,共享式上鎖允許多個(gè)線程擁有鎖。
針對(duì)問(wèn)題:對(duì)于一個(gè)線程寫,多個(gè)線程讀的場(chǎng)景,mutex
的效率很低。因?yàn)椴粌H讀與寫之間要加鎖,讀與讀之間也要加鎖,但是讀與讀之間的加鎖是不必要的,畢竟它不會(huì)改變數(shù)據(jù),于是就產(chǎn)生了可以同時(shí)讀的需求。
適用場(chǎng)景:有多個(gè)讀線程存在的時(shí)候,可以考慮讀寫鎖。
相關(guān)文章:
C++多線程——讀寫鎖shared_lock/shared_mutex
5. 遞歸鎖(recursive_mutex)
起始版本 | C++11 |
頭文件 | <mutex > |
接口 |
鎖定互斥。若另一線程已鎖定互斥,則到 |
嘗試鎖定互斥。立即返回。成功獲得鎖時(shí)返回 | |
解鎖互斥。要與 |
描述:recursive_mutex
與 mutex
唯一的區(qū)別就在于它可以在同一個(gè)線程里多次加鎖。
針對(duì)問(wèn)題:想象這樣一個(gè)場(chǎng)景,函數(shù)A調(diào)用了函數(shù)B,而且函數(shù)A和B都訪問(wèn)了一份共享數(shù)據(jù),這樣就可能造成死鎖。
適用場(chǎng)景:共享數(shù)據(jù)存在遞歸調(diào)用的時(shí)候。
相關(guān)文章:
遞歸鎖recursive_mutex的原理以及使用
6. 自旋鎖 (spinlock)
C++標(biāo)準(zhǔn)庫(kù)目前沒(méi)有實(shí)現(xiàn)自旋鎖。
描述:自旋鎖與互斥鎖(mutex
)的區(qū)別在于,mutex
調(diào)用lock
之后,會(huì)進(jìn)入睡眠狀態(tài),等到鎖可用了,再由cpu喚醒,再次獲取鎖;而自旋鎖不會(huì)進(jìn)去睡眠狀態(tài),會(huì)一直嘗試獲取鎖。這種一直嘗試獲取鎖的行為很耗cpu資源,所以要用在合適的場(chǎng)景。
用mutex就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的自旋鎖:
while(!mutex.try_lock()){}
針對(duì)問(wèn)題:考慮這樣一種情況,如果線程對(duì)共享資源占用時(shí)間非常短,也就是說(shuō)mutex
.lock
()等待的時(shí)間非常短,那么CPU將線程掛起再喚醒所耗費(fèi)的資源可能要大于一直嘗試加鎖。
適用場(chǎng)景:每個(gè)線程對(duì)共享資源占用時(shí)間非常短的情況。
相關(guān)文章:
c++之理解自旋鎖
二、RAII鎖
首先,什么是RAII?
RAII全稱是Resource Acquisition Is Initialization,翻譯過(guò)來(lái)是資源獲取即初始化,RAII機(jī)制用于管理資源的申請(qǐng)和釋放。對(duì)于資源,我們通常經(jīng)歷三個(gè)過(guò)程,申請(qǐng),使用,釋放,這里的資源不僅僅是內(nèi)存,也可以是文件、socket、鎖等等。但是我們往往只關(guān)注資源的申請(qǐng)和使用,而忘了釋放,這不僅會(huì)導(dǎo)致內(nèi)存泄漏,可能還會(huì)導(dǎo)致業(yè)務(wù)邏輯的錯(cuò)誤。c++之父給出了解決問(wèn)題的方案:RAII,它充分的利用了C++語(yǔ)言局部對(duì)象自動(dòng)銷毀的特性來(lái)控制資源的生命周期。
簡(jiǎn)而言之就是,將資源(內(nèi)存、socket、鎖等等)與一個(gè)局部變量綁定,這樣就可以避免我們忘記釋放資源,智能指針也是這樣的思想。
RAII鎖本質(zhì)上都是模板類,模板類型是這種鎖,也可以理解為對(duì)鎖的進(jìn)一步封裝。
1. lock_guard
起始版本 | C++11 |
頭文件 | <mutex> |
原型 | template< class Mutex > |
構(gòu)造函數(shù) | explicit lock_guard( mutex_type& m ); 等于調(diào)用 m.lock() 。 |
析構(gòu)函數(shù) | ~lock_guard(); 等效地調(diào)用 m.unlock() |
?描述:lock_guard的構(gòu)造函數(shù)需要傳入一個(gè)鎖,我們稱這個(gè)鎖為關(guān)聯(lián)鎖,然后在構(gòu)造函數(shù)內(nèi)部進(jìn)行加鎖,在析構(gòu)函數(shù)中進(jìn)行解鎖。
針對(duì)問(wèn)題:針對(duì)用戶可能忘記解鎖的問(wèn)題。
適用場(chǎng)景:建議用RAII鎖替換一般的鎖,這樣更加安全。
相關(guān)文章:
?? 走進(jìn)C++11? RAII風(fēng)格鎖std::lock_guard/std::unique_lock
2. unique_lock
起始版本 | C++11 |
頭文件 | <mutex> |
構(gòu)造函數(shù) | unique_lock的構(gòu)造函數(shù)比較多,包含了默認(rèn)構(gòu)造函數(shù),移動(dòng)構(gòu)造函數(shù),帶鎖構(gòu)造函數(shù),甚至還可以選擇加鎖的方式,我列幾個(gè): unique_lock() noexcept;???? 默認(rèn)構(gòu)造函數(shù),沒(méi)法直接加鎖,需要先調(diào)用=或者swap; explicit unique_lock( mutex_type& m );?? 帶鎖的構(gòu)造函數(shù),會(huì)調(diào)用m.lock(); unique_lock( mutex_type& m, std::defer_lock_t t ) ;? 只與鎖進(jìn)行關(guān)聯(lián),不執(zhí)行上鎖;? unique_lock( mutex_type& m, std::try_to_lock_t t );? 相當(dāng)于調(diào)用m.try_lock(); .........更多 |
析構(gòu)函數(shù) | ~unique_lock(); 若當(dāng)前線程擁有關(guān)聯(lián)互斥且獲得了其所有權(quán),則解鎖互斥。 |
上鎖 | void lock();??? 等同于m.lock(),m是與unique_lock關(guān)聯(lián)的鎖; bool try_lock();?? 等同于m.try_lock(); bool try_lock_for(time);? 等同于m.try_lock_for(time); bool try_lock_until(time); 等同于m.bool try_lock_until(); void unlock();??? 解鎖 |
修改unique_lock | void swap( unique_lock& other ) noexcept;???? 與另一 std::unique_lock 交換狀態(tài) mutex_type* release() noexcept;????? 將關(guān)聯(lián)互斥解關(guān)聯(lián)而不解鎖它 |
其他 | mutex_type* mutex() const noexcept;?? 返回指向關(guān)聯(lián)互斥的指針,或若無(wú)關(guān)聯(lián)鎖則null bool owns_lock() const noexcept;? 檢查當(dāng)前線程是否占有關(guān)聯(lián)鎖。 |
描述:unique_lock是lock_guard的加強(qiáng)版,首先在構(gòu)造函數(shù)中可以選擇是否與鎖關(guān)聯(lián),以及對(duì)鎖的操作類型;其次它還可以像鎖一樣調(diào)用lock(),try_lock(),unlock()等函數(shù),這使得unique_lock更加靈活;最后它增加了swap,release,owns_lock這些非常實(shí)用的接口。
針對(duì)問(wèn)題:lock_guard的問(wèn)題在于不夠靈活,這個(gè)不夠靈活首先體現(xiàn)在不能自由的釋放鎖,必須等到跳出作用域的時(shí)候調(diào)用析構(gòu)函數(shù)才能解鎖;再有就是lock_guard只提供了構(gòu)造函數(shù)和析構(gòu)函數(shù),沒(méi)有任何功能接口。
適用場(chǎng)景:建議用unique_lock替換lock_guard;
相關(guān)文章:
?? 走進(jìn)C++11 RAII風(fēng)格鎖std::lock_guard/std::unique_lock
3. shared_lock
起始版本 | C++14 |
頭文件 | <shared_mutex > |
構(gòu)造函數(shù) |
shared_mutex() noexcept;???? 默認(rèn)構(gòu)造函數(shù),沒(méi)法直接加鎖,需要先調(diào)用=或者swap; explicit shared_mutex( mutex_type& m );?? 帶鎖的構(gòu)造函數(shù),會(huì)調(diào)用m.lock(); shared_mutex( mutex_type& m, shared_mutex( mutex_type& m, std::try_to_lock_t t );? 相當(dāng)于調(diào)用m.try_lock(); .........更多 |
析構(gòu)函數(shù) | ~shared_mutex(); 若當(dāng)前線程擁有關(guān)聯(lián)互斥且獲得了其所有權(quán),則解鎖互斥。 |
上鎖 | void lock();??? 等同于m.lock_shared(),m是與unique_lock關(guān)聯(lián)的鎖; bool try_lock();?? 等同于m.try_lock_shared(); bool try_lock_for(time);? 等同于m.try_lock_shared_for(time); bool try_lock_until(time); 等同于m.bool try_lock_shared_until(); void unlock();???解鎖互斥, 等同于m.unlock_shared() |
修改shared_mutex | void swap( shared_mutex& other ) noexcept;???? 與另一 std::shared_mutex 交換狀態(tài) mutex_type* release() noexcept;????? 將關(guān)聯(lián)互斥解關(guān)聯(lián)而不解鎖它 |
其他 | mutex_type* mutex() const noexcept;?? 返回指向關(guān)聯(lián)互斥的指針,或若無(wú)關(guān)聯(lián)鎖則null bool owns_lock() const noexcept;? 檢查當(dāng)前線程是否占有關(guān)聯(lián)鎖。 |
描述:上邊這個(gè)表可以看出來(lái),shared_lock與unique_lock的接口一模一樣,他們唯一的區(qū)別在于,unique_lock是以排他式上鎖,同一時(shí)刻只允許一個(gè)線程獲取鎖;而shared_mutex是以共享式上鎖,允許多個(gè)線程同時(shí)獲得鎖。此外,shared_mutex只能與shared_mutex關(guān)聯(lián),因?yàn)閯e的鎖壓根沒(méi)有l(wèi)ock_shared接口。
針對(duì)問(wèn)題:shared_mutex算是對(duì)unique_lock的補(bǔ)充,因?yàn)閡nique_lock在存在多個(gè)只讀線程的情況時(shí),會(huì)有較大的性能損失,因?yàn)橹蛔x線程之間加鎖是不必要的。
適用場(chǎng)景:與shared_mutex一樣。
相關(guān)文章:
C++多線程——讀寫鎖shared_lock/shared_mutex
三、信號(hào)量
描述:
信號(hào)量 (semaphore) 是一種輕量的同步原件,主要用處是控制對(duì)共享資源的并發(fā)訪問(wèn)數(shù),說(shuō)白點(diǎn)就是控制同一時(shí)間訪問(wèn)某一資源的線程數(shù)。比如網(wǎng)吧,就那么多位置,滿了網(wǎng)管就不會(huì)給你開(kāi)機(jī)子,必須等有人下機(jī)才能上機(jī),這里的網(wǎng)吧就是共享資源,他有并發(fā)數(shù)量限制,網(wǎng)管就類似信號(hào)量的功能,限制網(wǎng)吧的同時(shí)訪問(wèn)數(shù)量。
信號(hào)量不是C++語(yǔ)言特有的概念,而是計(jì)算機(jī)學(xué)的概念,C++在C++20里才在標(biāo)準(zhǔn)庫(kù)里對(duì)其進(jìn)行了實(shí)現(xiàn)。
信號(hào)量?jī)?nèi)部會(huì)維護(hù)一個(gè)計(jì)數(shù)器,每當(dāng)有線程訪問(wèn)資源,就將計(jì)數(shù)器減1;當(dāng)有線程結(jié)束訪問(wèn),就將計(jì)數(shù)器加1。如果計(jì)數(shù)器不大于0,那么新的訪問(wèn)請(qǐng)求就需要等待。
C++20提供了兩種信號(hào)量,std::counting_semaphore和std::binary_semaphore,由于binary_semaphore是counting_semaphore的特例,因此這里主要介紹counting_semaphore。
起始版本 | C++20 |
頭文件 | <semaphore> |
構(gòu)造函數(shù) | constexpr explicit counting_semaphore( std::ptrdiff_t desired ); 創(chuàng)建一個(gè)計(jì)數(shù)器的值為 |
接口 | void acquire(); 若內(nèi)部計(jì)數(shù)器大于 ?0? 則嘗試將它減少 1 ,否則阻塞直至它大于 ?0? 。 在線程請(qǐng)求訪問(wèn)共享資源的時(shí)候調(diào)用。 |
void release( std::ptrdiff_t update = 1 ); 將內(nèi)部計(jì)數(shù)器的值增加 | |
bool try_acquire() noexcept; 嘗試將內(nèi)部計(jì)數(shù)器減少 1 ,成功返回true,失敗返回false,不會(huì)阻塞線程。 | |
template<class Rep, class Period> 嘗試將計(jì)數(shù)器減1,如果失敗則阻塞time時(shí)長(zhǎng) | |
template<class Clock, class Duration> 嘗試將計(jì)數(shù)器減1,如果失敗則阻塞至abs_time時(shí)刻 | |
constexpr std::ptrdiff_t max() noexcept; 返回最大并發(fā)數(shù) |
針對(duì)問(wèn)題:對(duì)于多個(gè)線程同時(shí)訪問(wèn)某個(gè)資源,shared_mutex是可以做到的,但是它不能控制并發(fā)的訪問(wèn)數(shù)量,而實(shí)際中很多計(jì)算機(jī)資源都存在并發(fā)限制,所以我們需要控制對(duì)這類資源的并發(fā)訪問(wèn)數(shù)量。
適用場(chǎng)景:適用于需要控制對(duì)資源的并發(fā)訪問(wèn)數(shù)量,比如線程池中控制線程數(shù)量,限制網(wǎng)絡(luò)連接數(shù)等等。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-617197.html
總結(jié)
C++20還增加了閂 (latch) 與屏障 (barrier),暫時(shí)不寫了,下次再寫。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-617197.html
到了這里,關(guān)于C++ 多線程編程(二) 各種各樣的鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!