一文搞懂多線程中各個難點
6.9讀寫鎖
6.9.1什么是讀寫鎖?
大部分情況下,對于共享變量的訪問特點:只是讀取共享變量的值,而不是修改,只有在少數(shù)情況下,才會真正的修改共享變量的值。
在這種情況下,讀請求之間是同步的,它們之間的并發(fā)訪問是安全的。然而寫請求必須鎖住讀請求和其它寫請求。
即讀線程可多個同時讀,而寫線程只允許同一時間內(nèi)一個線程去寫。
6.9.2讀寫鎖接口
#include
讀寫鎖的默認(rèn)屬性:
對于調(diào)用pthread_rwlock_init初始化的讀寫鎖,在不需要讀寫鎖的時候,需要調(diào)用pthread_rwlock_destroy銷毀。
6.9.3讀者加鎖
#include
最大的好處就是,允許多個線程以只讀加鎖的方式獲取到讀寫鎖;
本質(zhì)上,讀寫鎖的內(nèi)部維護(hù)了一個引用計數(shù),每當(dāng)線程以讀方式獲取讀寫鎖時,該引用計數(shù)+1;
當(dāng)釋放以讀加鎖的方式的讀寫鎖時,會先對引用計數(shù)進(jìn)行-1,直到引用計數(shù)的值為0的時候,才真正釋放了這把讀寫鎖。
6.9.4寫者加鎖
#include
寫鎖用的是獨占模式,如果當(dāng)前讀寫鎖被某寫線程占用著,則不允許任何讀鎖通過請求,也不允許任何寫鎖請求通過,讀鎖請求和寫鎖請求都要陷入阻塞,直到線程釋放寫鎖。
6.9.5 解鎖
#include
不論是讀者加鎖還是寫者加鎖,都采用該接口進(jìn)行解釋。
讀者解鎖,只有當(dāng)引用計數(shù)為0的時候,才真正釋放了讀寫鎖。
6.9.6讀寫鎖的競爭策略
對于讀寫鎖而言,目前有兩種策略,讀者優(yōu)先和攜著優(yōu)先;
讀寫鎖的類型有如下幾種:
PTHREAD_RWLOCK_PREFER_READER_NP, //讀者優(yōu)先
PTHREAD_RWLOCK_PREFER_WRITER_NP, //很唬人, 但是也是讀者優(yōu)先
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, //寫者優(yōu)先
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP
讀者優(yōu)先:讀鎖來請求可以立即響應(yīng),只要有一個讀鎖沒完成,那么寫鎖就無法寫。這種策略是不公平的,極端情況下,寫現(xiàn)場很可能被餓死,即線程總是拿不到鎖資源。
寫者優(yōu)先:只要線程申請了寫鎖,那么在寫鎖后面到來的讀鎖請求就會統(tǒng)統(tǒng)被阻塞,不能先于寫鎖拿到鎖。
讀寫鎖實現(xiàn)中的變量及含義
對于讀請求而言:如果
1. 無線程持有寫鎖,即_writer = 0.
2. 采用讀者優(yōu)先策略或者當(dāng)前沒有寫鎖申請請求,即 _nr_writers_queue = 0
3. 當(dāng)滿足這兩個條件時,讀鎖請求立即獲得讀鎖,返回之前執(zhí)行_nr_readers++,表示多了一個線程正在讀
4. 不滿足這兩個條件時,執(zhí)行_nr_readers_queued++,表示增加了一個讀鎖等待者,然后調(diào)用futex,陷入阻塞。醒來之后,執(zhí)行_nr_readers_queued- -,再次判斷是否滿足條件1,2
對于寫請求而言:如果
1. 無線程持有寫鎖,即_writer = 0.
2. 沒有線程持有讀鎖,即_nr_readers = 0.
3. 如果上述條件滿足,就會立即拿到鎖,將_writer 置為當(dāng)前線程的ID
4. 如果不滿足,則執(zhí)行_nr_writers_queue++, 表示增加了一個寫鎖等待者線程,然后執(zhí)行futex陷入等待。醒來后,先執(zhí)行_nr_writers_queue- -,再繼續(xù)判斷條件1,2
對于解鎖,如果當(dāng)前是寫鎖:
1. 執(zhí)行_writer = 0.,表示釋放寫鎖。
2. 根據(jù)_nr_writers_queue判斷有沒有寫鎖,如果有則喚醒一個寫鎖,如果沒有寫鎖等待者,則喚醒所有的讀鎖等待者。
對于解鎖,如果當(dāng)前是讀鎖:
1. 執(zhí)行_nr_readers- -,表示讀鎖占有者少了一個。
2. 判斷_nr_readers是否等于0,是的話則表示當(dāng)前線程是最后一個讀鎖占有者,需要喚醒寫鎖等待者或讀鎖等待者
3. 根據(jù)_nr_writers_queue判斷是否存在寫鎖等待者,若有,則喚醒一個寫鎖等待線程
4. 如果沒有寫鎖等待者,判斷是否存在讀鎖等待者,若有,則喚醒全部的讀鎖等待者
讀寫鎖很容易造成,讀者餓死或者寫者餓死。
也可以設(shè)計公平的讀寫鎖。
代碼:
#include
上述代碼很容易觸發(fā)線程餓死。
讀餓死或者寫?zhàn)I死。
7.線程間同步7.1為什么需要線程同步?
線程同步是為了對臨界資源訪問的合理性。
例如:
就像工廠里生產(chǎn)車間沒有原料了, 所有生產(chǎn)車間都停工了, 工人們都在車間睡覺。突然進(jìn)來一批原料, 如果原料充足, 你會發(fā)廣播給所有車間, 原料來了, 快來開工吧。如果進(jìn)來的原料很少, 只夠一個車間開工的, 你可能只會通知一個車間開工。
7.2如何做到線程間同步?
條件等待是線程間同步的另一種方法。
如果條件不滿足, 它能做的事情就是等待, 等到條件滿足為止。通常條件的達(dá)成, 很可能取決于另一個線程, 比如生產(chǎn)者-消費者模型。當(dāng)另外一個線程發(fā)現(xiàn)條件符合的時候, 它會選擇一個時機(jī)去通知等待在這個條件上的線程。有兩種可能性, 一種是喚醒一個線程, 一種是廣播, 喚醒其他線程。
則在這個情況下,需要做到:
1、線程在條件不滿足的情況下, 主動讓出互斥量, 讓其他線程去折騰, 線程在此處等待, 等待條件的滿足;
2、一旦條件滿足, 線程就可以立刻被喚醒。
3、線程之所以可以安心等待, 依賴的是其他線程的協(xié)作, 它確信會有一個線程在發(fā)現(xiàn)條件滿足以后, 將向它發(fā)送信號, 并且讓出互斥量。
7.3條件變量
本質(zhì)上是PCB等待隊列 + 等待接口 + 喚醒接口。
7.3.1條件變量的初始化
靜態(tài)初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
動態(tài)初始化
pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
7.3.2條件變量的等待
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict conpthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
為什么這兩個接口中有互斥鎖?
條件不會無緣無故地突然變得滿足了, 必然會牽扯到共享數(shù)據(jù)的變化。所以一定要有互斥鎖來保護(hù)。沒有互斥鎖, 就無法安全地獲取和修改共享數(shù)據(jù)。
同步并沒有保證互斥,而保證互斥是使用到了互斥鎖。
pthread_mutex_lock(&m)
while(condition_is_false)
{
pthread_mutex_unlock(&m);
//解鎖之后, 等待之前, 可能條件已經(jīng)滿足, 信號已經(jīng)發(fā)出, 但是該信號可能會被錯過
cond_wait(&cv);
pthread_mutex_lock(&m);
}
上面的解鎖和等待不是原子操作。解鎖以后, 調(diào)用cond_wait之前,如果已經(jīng)有其他線程獲取到了互斥量, 并且滿足了條件, 同時發(fā)出了通知信號, 那么cond_wait將錯過這個信號, 可能會導(dǎo)致線程永遠(yuǎn)處于阻塞狀態(tài)。所以解鎖加等待必須是一個原子性的操作, 以確保已經(jīng)注冊到事件的等待隊列之前, 不會有其他線程可以獲得互斥量。
那先注冊等待事件, 后釋放鎖不行嗎?注意, 條件等待是個阻塞型的接口, 不單單是注冊在事件的等待隊列上, 線程也會因此阻塞于此, 從而導(dǎo)致互斥量無法釋放, 其他線程獲取不到互斥量, 也就無法通過改變共享數(shù)據(jù)使等待的條件得到滿足, 因此這就造成了死鎖。
pthread_mutex_lock(&m);
while(condition_is_false)
pthread_cond_wait(&v,&m);//此處會阻塞
如果代碼運行到此處, 則表示我們等待的條件已經(jīng)滿足了,
*并且在此持有了互斥量
在滿足條件的情況下, 做你想做的事情。
pthread_mutex_unlock(&m);
pthread_cond_wait函數(shù)只能由擁有互斥量的線程來調(diào)用, 當(dāng)該函數(shù)返回的時候, 系統(tǒng)會確保該線程再次持有互斥量, 所以這個接口容易給人一種誤解, 就是該線程一直在持有互斥量。事實上并不是這樣的。這個接口向系統(tǒng)聲明了我在PCB等待序列中之后, 就把互斥量給釋放了。這樣其他線程就有機(jī)會持有互斥量,操作共享數(shù)據(jù), 觸發(fā)變化, 使線程等待的條件得到滿足。
pthread_cond_wait內(nèi)部會進(jìn)行解鎖邏輯,則一定要先放到PCB等待序列中,再進(jìn)行解鎖。
while(condition_is_false)
pthread_cond_wait(&v,&m);//此處會阻塞
if(condition_is_false)
pthread_cond_wait(&v,&m);//此處會阻塞
喚醒以后, 再次檢查條件是否滿足, 是不是多此一舉?
因為喚醒中存在虛假喚醒(spurious wakeup) , 換言之,條件尚未滿足, pthread_cond_wait就返了。在一些實現(xiàn)中, 即使沒有其他線程向條件變量發(fā)送信號, 等待此條件變量的線程也有可能會醒來。
條件滿足了發(fā)送信號, 但等到調(diào)用pthread_cond_wait的線程得到CPU資源時, 條件又再次不滿足了。好在無論是哪種情況, 醒來之后再次測試條件是否滿足就可以解決虛假等待的問題。
pthread_cond_wait內(nèi)部實現(xiàn)邏輯:
將調(diào)用pthread_cond_wait函數(shù)的執(zhí)行流放入到PCB等待隊列當(dāng)中
解鎖
等待被喚醒
被喚醒之后:
1、從PCB等待隊列中移除出來
2、搶占互斥鎖
情況1:拿到互斥鎖,pthread_cond_wait就返回了
情況2:沒有拿到互斥鎖,阻塞在pthread_cond_wait內(nèi)部搶鎖的邏輯中
當(dāng)阻塞在pthread_cond_wait函數(shù)搶鎖邏輯中時,一旦執(zhí)行流時間耗盡,意味著線程就被切換出來了,程序計數(shù)器就保存的是搶鎖的指令,上下文信息保存的就是寄存器的值
當(dāng)再次擁有CPU資源后,恢復(fù)搶鎖邏輯
直到搶鎖成功,pthread_cond_wait函數(shù)才會返回
7.3.3條件變量的喚醒
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_signal負(fù)責(zé)喚醒等待在條件變量上的一個線程。
pthread_cond_broadcast,就是廣播喚醒等待在條件變量上的所有線程。
先發(fā)送信號,然后解鎖互斥量,這個順序是必須的嘛?
先通知條件變量、 后解鎖互斥量, 效率會比先解鎖、 后通知條件變量低。因為先通知后解鎖, 執(zhí)行pthread_cond_wait的線程可能在互斥量已然處于加鎖狀態(tài)的時候醒來, 發(fā)現(xiàn)互斥量仍然沒有解鎖, 就會再次休眠, 從而導(dǎo)致了多余的上下文切換。
7.3.4條件變量的銷毀
int pthread_cond_destroy(pthread_cond_t *cond);
注意:
1、永遠(yuǎn)不要用一個條件變量對另一個條件變量賦值, 即pthread_cond_t cond_b = cond_a不合法, 這種行為是未定義的。
2、使用PTHREAD_COND_INITIALIZE靜態(tài)初始化的條件變量, 不需要被銷毀。
3、要調(diào)用pthread_cond_destroy銷毀的條件變量可以調(diào)用pthread_cond_init重新進(jìn)行初始化。
4、不要引用已經(jīng)銷毀的條件變量, 這種行為是未定義的。
例:
#include
在這里為什么有兩個條件變量呢?
若所有的線程只使用一個條件變量,會導(dǎo)致所有線程最后都進(jìn)入PCB等待隊列。
thread apply all bt查看:
7.3.5情況分析:兩個生產(chǎn)者,兩個消費者,一個PCB等待隊列
1、最開始的情況,兩個消費者搶到了鎖,此時生產(chǎn)者未生產(chǎn),則都放入PCB等待隊列中
2、一個生產(chǎn)者搶到了鎖,生產(chǎn)了一份材料,喚醒一個消費者,此時三者搶鎖,若兩個生產(chǎn)者分別先后搶到了鎖,則都進(jìn)入PCB等待隊列中
3、只有一個消費者,則必會搶到鎖,消費材料,喚醒PCB等待隊列,若此時喚醒的是,消費者,則現(xiàn)在是這樣一個情況:
4、兩個消費者在外邊搶鎖,一定都會進(jìn)入PCB等待隊列中
解決上述問題可采用兩種方法:
1、使用int pthread_cond_broadcast(pthread_cond_t *cond);,喚醒PCB等待隊列中所有的線程。此時所有線程都會同時執(zhí)行搶鎖邏輯,太消費資源了。此方法不妥
2、采用兩個PCB等待序列,一個放生產(chǎn)者,一個放消費者,生產(chǎn)者喚醒消費者,消費者喚醒生產(chǎn)者。
8.線程取消8.1線程取消函數(shù)接口int pthread_cancel(pthread_t thread);
一個線程可以通過調(diào)用該函數(shù)向另一個線程發(fā)送取消請求。這不是個阻塞型接口, 發(fā)出請求后, 函數(shù)就立刻返回了, 而不會等待目標(biāo)線程退出之后才返回。
調(diào)用pthread_cancel時, 會向目標(biāo)線程發(fā)送一個SIGCANCEL的信號, 該信號就是kill -l中消失的32號信號。
線程的默認(rèn)取消狀態(tài)是PTHREAD_CANCEL_ENABLE。即是可被取消的。
什么是取消點?可通過man pthreads查看取消點
就是對于某些函數(shù), 如果線程允許取消且取消類型是延遲取消, 并且線程也收到了取消請求, 那么當(dāng)執(zhí)行到這些函數(shù)的時候, 線程就可以退出了。
8.2線程取消帶來的弊端
目標(biāo)線程可能會持有互斥量、 信號量或其他類型的鎖, 這時候如果收到取消請求, 并且取消類型是異步取消, 那么可能目標(biāo)線程掌握的資源還沒有來得及釋放就被迫退出了, 這可能會給其他線程帶來不可恢復(fù)的后果, 比如死鎖(其他線程再也無法獲得資源) 。
注意:
輕易不要調(diào)用pthread_cancel函數(shù), 在外部殺死線程是很糟糕的做法,畢竟如果想通知目標(biāo)線程退出, 還可以采取其他方法。
如果不得不允許線程取消, 那么在某些非常關(guān)鍵不容有失的代碼區(qū)域, 暫時將線程設(shè)置成不可取消狀態(tài), 退出關(guān)鍵區(qū)域之后, 再恢復(fù)成可以取消的狀態(tài)。
在非關(guān)鍵的區(qū)域, 也要將線程設(shè)置成延遲取消, 永遠(yuǎn)不要設(shè)置成異步取消。
8.2線程清理函數(shù)
假設(shè)遇到取消請求, 線程執(zhí)行到了取消點, 卻沒有來得及做清理動作(如動態(tài)申請的內(nèi)存沒有釋放, 申請的互斥量沒有解鎖等) , 可能會導(dǎo)致錯誤的產(chǎn)生, 比如死鎖, 甚至是進(jìn)程崩潰。
為了避免這種情況, 線程可以設(shè)置一個或多個清理函數(shù), 線程取消或退出時,會自動執(zhí)行這些清理函數(shù), 以確保資源處于一致的狀態(tài)。
如果線程被取消, 清理函數(shù)則會負(fù)責(zé)解鎖操作。
void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);
這兩個函數(shù)必須同時出現(xiàn), 并且屬于同一個語法塊。
何時會觸發(fā)注冊的清理函數(shù):?
1、當(dāng)線程的主函數(shù)是調(diào)用pthread_exit返回的, 清理函數(shù)總是會被執(zhí)行。
2、當(dāng)線程是被其他線程調(diào)用pthread_cancel取消的, 清理函數(shù)總是會被執(zhí)行。
3、當(dāng)線程的主函數(shù)是通過return返回的, 并且pthread_cleanup_pop的唯一參數(shù)execute是0時, 清理函數(shù)不會被執(zhí)行.
4、線程的主函數(shù)是通過return返回的, 并且pthread_cleanup_pop的唯一參數(shù)execute是非零值時, 清理函數(shù)會執(zhí)行一次。
代碼:
#include
結(jié)果:只要拿到鎖,就表明線程清理函數(shù)成功了。
9.多線程與fork()
永遠(yuǎn)不要在多線程程序里面調(diào)用fork。
Linux的fork函數(shù), 會復(fù)制一個進(jìn)程, 對于多線程程序而言, fork函數(shù)復(fù)制的是用fork的那個線程, 而并不復(fù)制其他的線程。fork之后其他線程都不見了。Linux存在forkall語義的系統(tǒng)調(diào)用, 無法做到將多線程全部復(fù)制。
多線程程序在fork之前, 其他線程可能正持有互斥量處理臨界區(qū)的代碼。fork之后, 其他線程都不見了, 那么互斥量的值可能處于不可用的狀態(tài), 也不會有其他線程來將互斥量解鎖。
10.生產(chǎn)者與消費者模型10.1生產(chǎn)者與消費者模型的本質(zhì)
本質(zhì)上是一個線程安全的隊列,和兩種角色的線程(生產(chǎn)者和消費者)
存在三種關(guān)系:
1、生產(chǎn)者與生產(chǎn)者互斥
2、消費者與消費者互斥
3、生產(chǎn)者與消費者同步+互斥
10.2為什么需要生產(chǎn)者與消費者模型?
生產(chǎn)者和消費者彼此之間不直接通訊,而通過阻塞隊列來進(jìn)行通訊,所以生產(chǎn)者生成完數(shù)據(jù)之后不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產(chǎn)者要數(shù)據(jù),而是直接從阻塞隊列中取,阻塞隊列就相當(dāng)于一個緩沖區(qū),平衡了生產(chǎn)者和消費者的處理能力。這個阻塞隊列就是用來給生產(chǎn)者和消費解耦的。
10.3優(yōu)點
1、解耦
2、支持高并發(fā)
3、支持忙閑不均
10.4實現(xiàn)兩個消費者線程,兩個生產(chǎn)者線程的生產(chǎn)者消費者模型
生產(chǎn)者生成時用的同一個全局變量,故對該全局變量進(jìn)行了加鎖。
#include
先考慮代碼的核心邏輯(先實現(xiàn))
考慮核心邏輯中是否訪問臨界資源或者說執(zhí)行臨界區(qū)代碼,如果有就需要保持互斥
考慮線程之間是否需要同步
請輸入評論內(nèi)容...
請輸入評論/評論長度6~500個字
最新活動更多
-
即日-11.13立即報名>>> 【在線會議】多物理場仿真助跑新能源汽車
-
11月28日立即報名>>> 2024工程師系列—工業(yè)電子技術(shù)在線會議
-
12月19日立即報名>> 【線下會議】OFweek 2024(第九屆)物聯(lián)網(wǎng)產(chǎn)業(yè)大會
-
即日-12.26火熱報名中>> OFweek2024中國智造CIO在線峰會
-
即日-2025.8.1立即下載>> 《2024智能制造產(chǎn)業(yè)高端化、智能化、綠色化發(fā)展藍(lán)皮書》
-
精彩回顧立即查看>> 【限時免費下載】TE暖通空調(diào)系統(tǒng)高效可靠的組件解決方案
推薦專題
- 高級軟件工程師 廣東省/深圳市
- 自動化高級工程師 廣東省/深圳市
- 光器件研發(fā)工程師 福建省/福州市
- 銷售總監(jiān)(光器件) 北京市/海淀區(qū)
- 激光器高級銷售經(jīng)理 上海市/虹口區(qū)
- 光器件物理工程師 北京市/海淀區(qū)
- 激光研發(fā)工程師 北京市/昌平區(qū)
- 技術(shù)專家 廣東省/江門市
- 封裝工程師 北京市/海淀區(qū)
- 結(jié)構(gòu)工程師 廣東省/深圳市