訂閱
糾錯
加入自媒體

一文搞懂多線程中各個難點

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ū)代碼,如果有就需要保持互斥

考慮線程之間是否需要同步

<上一頁  1  2  3  4  
聲明: 本文由入駐維科號的作者撰寫,觀點僅代表作者本人,不代表OFweek立場。如有侵權(quán)或其他問題,請聯(lián)系舉報。

發(fā)表評論

0條評論,0人參與

請輸入評論內(nèi)容...

請輸入評論/評論長度6~500個字

您提交的評論過于頻繁,請輸入驗證碼繼續(xù)

暫無評論

暫無評論

人工智能 獵頭職位 更多
掃碼關(guān)注公眾號
OFweek人工智能網(wǎng)
獲取更多精彩內(nèi)容
文章糾錯
x
*文字標(biāo)題:
*糾錯內(nèi)容:
聯(lián)系郵箱:
*驗 證 碼:

粵公網(wǎng)安備 44030502002758號