鎖是一個(gè)常見(jiàn)的同步概念,我們都聽(tīng)說(shuō)過(guò)加鎖(lock)或者解鎖(unlock),當(dāng)然學(xué)術(shù)一點(diǎn)的說(shuō)法是獲取(acquire)和釋放(release)。
恰好pthread包含這幾種鎖的API,而C++11只包含其中的部分。接下來(lái)我主要通過(guò)pthread的API來(lái)展開(kāi)本文。
mutex(互斥量)
mutex(mutual exclusive)即互斥量(互斥體)。也便是常說(shuō)的互斥鎖。
盡管名稱(chēng)不含lock,但是稱(chēng)之為鎖,也是沒(méi)有太大問(wèn)題的。mutex無(wú)疑是最常見(jiàn)的多線程同步方式。其思想簡(jiǎn)單粗暴,多線程共享一個(gè)互斥量,然后線程之間去競(jìng)爭(zhēng)。得到鎖的線程可以進(jìn)入臨界區(qū)執(zhí)行代碼。
//聲明一個(gè)互斥量
pthread_mutex_tmtx;
//初始化
pthread_mutex_init(&mtx,NULL);
//加鎖
pthread_mutex_lock(&mtx);
//解鎖
pthread_mutex_unlock(&mtx);
//銷(xiāo)毀
pthread_mutex_destroy(&mtx);
mutex是睡眠等待(sleep waiting)類(lèi)型的鎖,當(dāng)線程搶互斥鎖失敗的時(shí)候,線程會(huì)陷入休眠。優(yōu)點(diǎn)就是節(jié)省CPU資源,缺點(diǎn)就是休眠喚醒會(huì)消耗一點(diǎn)時(shí)間。另外自從Linux 2.6版以后,mutex完全用futex的API實(shí)現(xiàn)了,內(nèi)部系統(tǒng)調(diào)用的開(kāi)銷(xiāo)大大減小。
值得一提的是,pthread的鎖一般都有一個(gè)trylock的函數(shù),比如對(duì)于互斥量:
ret=pthread_mutex_trylock(&mtx);
if(0==ret){//加鎖成功
...
pthread_mutex_unlock(&mtx);
}elseif(EBUSY==ret){//鎖正在被使用;
...
}
pthread_mutex_trylock用于以非阻塞的模式來(lái)請(qǐng)求互斥量。就好比各種IO函數(shù)都有一個(gè)noblock的模式一樣,對(duì)于加鎖這件事也有類(lèi)似的非阻塞模式。
當(dāng)線程嘗試加鎖時(shí),如果鎖已經(jīng)被其他線程鎖定,該線程就會(huì)阻塞住,直到能成功acquire。但有時(shí)候我們不希望這樣。
pthread_mutex_trylock在被其他線程鎖定時(shí),會(huì)返回特殊錯(cuò)誤碼。加鎖成返回0,僅當(dāng)成功但時(shí)候,我們才能解鎖在后面進(jìn)行解鎖操作!
C++11開(kāi)始引入了多線程庫(kù)
此外,依據(jù)同一線程是否能多次加鎖,把互斥量又分為如下兩類(lèi):
- 是:稱(chēng)為『遞歸互斥量』recursive mutex ,也稱(chēng)『可重入鎖』reentrant lock
- 否:即『非遞歸互斥量』non-recursive mute),也稱(chēng)『不可重入鎖』non-reentrant mutex
若同一線程對(duì)非遞歸的互斥量多次加鎖,可能會(huì)造成死鎖。遞歸互斥量則無(wú)此風(fēng)險(xiǎn)。C++11中有遞歸互斥量的API:std::recursive_mutex。對(duì)于pthread則可以通過(guò)給mutex添加PTHREAD_MUTEX_RECURSIVE 屬性的方式來(lái)使用遞歸互斥量:
//聲明一個(gè)互斥量
pthread_mutex_tmtx;
//聲明一個(gè)互斥量的屬性變量
pthread_mutexattr_tmtx_attr;
//初始化互斥量的屬性變量
pthread_mutexattr_init(&mtx_attr);
//設(shè)置遞歸互斥量的屬性
pthread_mutexattr_settype(&mtx_attr,PTHREAD_MUTEX_RECURSIVE);
//把屬性賦值給互斥量
pthread_mutext_init(&mtx,&mutext_attr);
然而對(duì)于遞歸互斥量或者說(shuō)可重入鎖的使用則需要克制。Stevens大神生前在《APUE》中說(shuō)『使用好它是十分tricky的,僅當(dāng)沒(méi)有其他解決方案時(shí)才使用』。
可重入鎖這個(gè)概念和稱(chēng)呼的走俏多半是Java語(yǔ)言的功勞。
condition variable(條件變量)
請(qǐng)注意條件變量不是鎖,它是一種線程間的通訊機(jī)制,并且?guī)缀蹩偸呛突コ饬恳黄鹗褂玫摹K曰コ饬亢蜅l件變量二者一般是成套出現(xiàn)的。比如C++11中也有條件變量的API:std::condition_variable。
對(duì)于pthread:
//聲明一個(gè)互斥量
pthread_mutex_tmtx;
//聲明一個(gè)條件變量
pthread_cond_tcond;
...
//初始化
pthread_mutex_init(&mtx,NULL);
pthread_cond_init(&cond,NULL);
//加鎖
pthread_mutex_lock(&mtx);
//加鎖成功,等待條件變量觸發(fā)
pthread_cond_wait(&cond,&mtx);
...
//加鎖
pthread_mutex_lock(&mtx);
pthread_cond_signal(&cond);
...
//解鎖
pthread_mutex_unlock(&mtx);
//銷(xiāo)毀
pthread_mutex_destroy(&mtx)
pthread_cond_wait函數(shù)會(huì)把條件變量和互斥量都傳入。并且多線程調(diào)用的時(shí)候條件變量和互斥量一定要一一對(duì)應(yīng),不能一個(gè)條件變量在不同線程中wait的時(shí)候傳入不同的互斥量。否則是未定義結(jié)果。
關(guān)于是先解鎖互斥量還是先進(jìn)行條件變量的通知,是另外一個(gè)比較大的議題。有種論斷說(shuō):先解鎖互斥量再通知條件變量可以減少多余的上下文切換,進(jìn)而提高效率。這種說(shuō)法是基于一種實(shí)現(xiàn)假設(shè):先通知條件變量,再解鎖。
可能讓其他等待條件變量的線程被喚醒了,但是此時(shí)互斥量還沒(méi)解鎖,從而再次陷入休眠。然而對(duì)于另外一些實(shí)現(xiàn),比如Linux系統(tǒng),則通過(guò)等待變形(wait morphing)解決了這一問(wèn)題。所以先通知再解鎖也沒(méi)用問(wèn)題。
另外在使用條件變量的過(guò)程中有個(gè)稍微違反直覺(jué)的寫(xiě)法:那就是使用while而不是if來(lái)做判斷狀態(tài)是否滿足。這樣做的原因有二:
- 避免驚群;
- 避免某些情況下線程被虛假喚醒(即沒(méi)有pthread_cond_signal就解除了阻塞)。
比如半同步/半reactor的網(wǎng)絡(luò)模型中,在工作線程消費(fèi)fd隊(duì)列的時(shí)候:
while(1){
if(pthread_mutex_lock(&mtx)!=0){//加鎖
...//異常邏輯
}
while(!queue.empty()){
if(pthread_cond_wait(&cond,&mtx)!=0){
...//異常邏輯
}
}
autodata=queue.pop();
if(pthread_mutex_unlock(&mtx)!=0){//解鎖
...//異常邏輯
}
process(data);//處理流程,業(yè)務(wù)邏輯
}
read-write lock(讀寫(xiě)鎖)
顧名思義『讀寫(xiě)鎖』就是對(duì)于臨界區(qū)區(qū)分讀和寫(xiě)。在讀多寫(xiě)少的場(chǎng)景下,不加區(qū)分的使用互斥量顯然是有點(diǎn)浪費(fèi)的。此時(shí)便該上演讀寫(xiě)鎖的拿手好戲。
讀寫(xiě)鎖有一個(gè)別稱(chēng)叫『共享-獨(dú)占鎖』。不過(guò)單看『共享-獨(dú)占鎖』或者『讀寫(xiě)鎖』這兩個(gè)名稱(chēng),其實(shí)并未區(qū)分對(duì)于讀和寫(xiě),到底誰(shuí)共享,誰(shuí)獨(dú)占。可能會(huì)讓人誤以為讀寫(xiě)鎖是一種更為泛化的稱(chēng)呼,其實(shí)不是。讀寫(xiě)鎖的含義是準(zhǔn)確的:是一種 讀共享,寫(xiě)?yīng)氄嫉逆i。
讀寫(xiě)鎖的特性:
- 當(dāng)讀寫(xiě)鎖被加了寫(xiě)鎖時(shí),其他線程對(duì)該鎖加讀鎖或者寫(xiě)鎖都會(huì)阻塞(不是失敗)。
- 當(dāng)讀寫(xiě)鎖被加了讀鎖時(shí),其他線程對(duì)該鎖加寫(xiě)鎖會(huì)阻塞,加讀鎖會(huì)成功。
因而適用于多讀少寫(xiě)的場(chǎng)景。
//聲明一個(gè)讀寫(xiě)鎖
pthread_rwlock_trwlock;
...
//在讀之前加讀鎖
pthread_rwlock_rdlock(&rwlock);
...共享資源的讀操作
//讀完釋放鎖
pthread_rwlock_unlock(&rwlock);
//在寫(xiě)之前加寫(xiě)鎖
pthread_rwlock_wrlock(&rwlock);
...共享資源的寫(xiě)操作
//寫(xiě)完釋放鎖
pthread_rwlock_unlock(&rwlock);
//銷(xiāo)毀讀寫(xiě)鎖
pthread_rwlock_destroy(&rwlock);
其實(shí)加讀鎖和加寫(xiě)鎖這兩個(gè)說(shuō)法可能會(huì)造成誤導(dǎo),讓人誤以為是有兩把鎖,其實(shí)讀寫(xiě)鎖是一個(gè)鎖。所謂加讀鎖和加寫(xiě)鎖,準(zhǔn)確的說(shuō)法可能是『給讀寫(xiě)鎖加讀模式的鎖定和加寫(xiě)模式的鎖定』。
讀寫(xiě)鎖和互斥量一樣也有trylock函數(shù),也是以非阻塞地形式來(lái)請(qǐng)求鎖,不會(huì)導(dǎo)致阻塞。
pthread_rwlock_tryrdlock(&rwlock)
pthread_rwlock_trywrlock(&rwlock)
C++11中有互斥量、條件變量但是并沒(méi)有引入讀寫(xiě)鎖。而在C++17中出現(xiàn)了一種新鎖:std::shared_mutex。用它可以模擬實(shí)現(xiàn)出讀寫(xiě)鎖。demo代碼可以直接參考cppreference:
https://en.cppreference.com/w/cpp/thread/shared_mutex
另外多讀少寫(xiě)的場(chǎng)景有些特殊場(chǎng)景,可以用特殊的數(shù)據(jù)結(jié)構(gòu)減少鎖使用:
- 多讀單寫(xiě)的線性數(shù)據(jù)。用數(shù)組實(shí)現(xiàn)環(huán)形隊(duì)列,避免vector等動(dòng)態(tài)擴(kuò)張的數(shù)據(jù)結(jié)構(gòu),寫(xiě)在結(jié)尾,由于單寫(xiě)因而可以不加鎖;讀在開(kāi)頭,由于多讀(避免重復(fù)消費(fèi))所以需要加一下鎖(互斥量就行)。
- 多讀單寫(xiě)的KV。可以使用雙緩沖(double buffer)的數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)。double buffer同名的概念比較多,這里指的是foreground 和 backgroud 兩個(gè)buffer進(jìn)行切換的『0 - 1切換』技術(shù)。比如實(shí)現(xiàn)動(dòng)態(tài)加載(熱加載)配置文件的時(shí)候。可能會(huì)在切換間隙加一個(gè)短暫的互斥量,但是基本可以認(rèn)為是lock free的。
我一張口,你就會(huì)發(fā)現(xiàn):無(wú)非是空間換時(shí)間的老套路了。
spinlock(自旋鎖)
自旋之名頗為玄妙,第一次聽(tīng)聞常讓人略覺(jué)高大。但和無(wú)數(shù)個(gè)好似『故意把簡(jiǎn)單概念復(fù)雜化』的計(jì)算機(jī)術(shù)語(yǔ)一樣,自旋鎖的本質(zhì)簡(jiǎn)單的難以置信。
要了解自旋鎖,首先了解自旋。什么是自旋(spin)呢?更為通俗的一個(gè)詞是『忙等待』(busy waiting)。最最通俗的一個(gè)理解,其實(shí)就是死循環(huán)……。
單看使用方法和使用互斥量的代碼是差不多的。只不過(guò)自旋鎖不會(huì)引起線程休眠。當(dāng)共享資源的狀態(tài)不滿足的時(shí)候,自旋鎖會(huì)不停地循環(huán)檢測(cè)狀態(tài)。因?yàn)椴粫?huì)陷入休眠,而是忙等待的方式也就不需要條件變量。
這是優(yōu)點(diǎn)也是缺點(diǎn)。不休眠就不會(huì)引起上下文切換,但是會(huì)比較浪費(fèi)CPU。
//聲明一個(gè)自旋鎖變量
pthread_spinlock_tspinlock;
//初始化
pthread_spin_init(&spinlock,0);
//加鎖
pthread_spin_lock(&spinlock);
//解鎖
pthread_spin_unlock(&spinlock);
//銷(xiāo)毀
pthread_spin_destroy(&spinlock);
pthread_spin_init函數(shù)的第二個(gè)參數(shù)名為pshared(int類(lèi)型)。表示的是是否能進(jìn)程間共享自旋鎖。這被稱(chēng)之為T(mén)hread Process-Shared Synchronization。互斥量的通過(guò)屬性也可以把互斥量設(shè)置成進(jìn)程間共享的。pshared有兩個(gè)枚舉值:
- PTHREAD_PROCESS_PRIVATE:僅同進(jìn)程下讀線程可以使用該自旋鎖
- PTHREAD_PROCESS_SHARED:不同進(jìn)程下的線程可以使用該自旋鎖
在Linux上的glibc中這兩個(gè)枚舉值分別是0和1(Mac上不是)。所以通常也會(huì)看到直接傳0的代碼。你可能覺(jué)得不使用宏,直接用數(shù)字硬編碼不是一個(gè)好習(xí)慣。的確,妥妥的Magic Number,但還有一個(gè)有趣的事實(shí)你需要了解:并不是所有實(shí)現(xiàn)都支持自旋鎖設(shè)置pshared。比如:
intpthread_spin_init(pthread_spinlock_t*lock,intpshared){
/*RelaxedMOisfinebecausethisisaninitializingstore.*/
atomic_store_relaxed(lock,0);
return0;
}
所以直接傳0可能也無(wú)傷大雅。
自旋鎖 VS 互斥量+條件變量 孰優(yōu)孰劣?肯定要看具體的使用場(chǎng)景,(我好像在說(shuō)片湯話)。當(dāng)你不知道在你的使用場(chǎng)景下這兩種鎖該用哪個(gè)的時(shí)候,那就是用互斥量吧!
或者通過(guò)壓測(cè)的判斷,不過(guò)大多數(shù)時(shí)候我們好像并不需要這么一個(gè)pthread的自旋鎖,知友們可以提供一些自旋鎖的使用參考。
-
cpu
+關(guān)注
關(guān)注
68文章
11277瀏覽量
224938 -
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7335瀏覽量
94754 -
Mac
+關(guān)注
關(guān)注
0文章
1127瀏覽量
55309 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4417瀏覽量
67499
原文標(biāo)題:如何理解互斥鎖、條件變量、讀寫(xiě)鎖以及自旋鎖?
文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
探索TMAG5115:高性能霍爾效應(yīng)鎖存器的卓越特性與應(yīng)用
面試必看!排隊(duì)自旋鎖32位變量的域劃分與核心作用
面試必看:排隊(duì)自旋鎖之MCS鎖的實(shí)現(xiàn)原理與關(guān)鍵考點(diǎn)
深入解析CD4514B與CD4515B:4位鎖存/4到16線解碼器
【「Linux 設(shè)備驅(qū)動(dòng)開(kāi)發(fā)(第 2 版)」閱讀體驗(yàn)】Linux內(nèi)核開(kāi)發(fā)基礎(chǔ)
【「Linux 設(shè)備驅(qū)動(dòng)開(kāi)發(fā)(第 2 版)」閱讀體驗(yàn)】+讀內(nèi)核處理的核心輔助函數(shù)
飛凌嵌入式ElfBoard-文件I/O的深入學(xué)習(xí)之文件鎖
霍爾元件在電子鎖里是如何應(yīng)用的?用幾顆?
五大認(rèn)證加冕!德施曼以“首創(chuàng)AI智能鎖”重構(gòu)智能鎖競(jìng)爭(zhēng)格局
五大認(rèn)證加冕!德施曼以“首創(chuàng)AI智能鎖”重構(gòu)智能鎖競(jìng)爭(zhēng)格局
官網(wǎng)nrf24l01的例程demo會(huì)出現(xiàn)互斥鎖報(bào)錯(cuò)是為什么?
屏顯智能電子鎖語(yǔ)音芯片方案新選擇
什么是電磁鎖控制板?24路鎖控板的使用步驟概述
鎖聯(lián)世界,智啟萬(wàn)家,華普微藍(lán)牙智能鎖解決方案
WT3000A,電子鎖 AI 智慧語(yǔ)音方案強(qiáng)勢(shì)來(lái)襲!
深入了解互斥鎖、條件變量、讀寫(xiě)鎖以及自旋鎖
評(píng)論