国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

這篇具有很好參考價(jià)值的文章主要介紹了【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

人生總是那么痛苦嗎?還是只有小時(shí)候是這樣? —總是如此

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型



一、線程互斥

1.多線程共享資源訪問的不安全問題

1.
假設(shè)現(xiàn)在有一份共享資源tickets,如果我們想讓多個(gè)線程都對(duì)這個(gè)資源進(jìn)行操作,也就是tickets- -的操作,但下面兩份代碼分別出現(xiàn)了不同的結(jié)果,上面代碼并沒有出現(xiàn)問題,而下面代碼卻出現(xiàn)了票為負(fù)數(shù)的情況,這是怎么回事呢?
其實(shí)問題產(chǎn)生就是由于多線程被調(diào)度器調(diào)度的特性導(dǎo)致的。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.
了解上面的問題需要知道線程調(diào)度的特性,實(shí)際線程在被調(diào)度時(shí)他的上下文會(huì)被加載到CPU的寄存器中,而線程在被切換的時(shí)候,線程又會(huì)帶著自己的上下文被切換下去,此時(shí)要進(jìn)行線程的上下文保存,以便于下次該線程被切換上來的時(shí)候能夠進(jìn)行上下文數(shù)據(jù)的恢復(fù)。
除此之外,像tickets- -這樣的操作,對(duì)應(yīng)的匯編指令其實(shí)至少有三條,1.讀取數(shù)據(jù) 2.修改數(shù)據(jù) 3.寫回?cái)?shù)據(jù),而線程函數(shù)我們知道會(huì)在每個(gè)線程的私有棧都存在一份,在上面的例子中多個(gè)線程執(zhí)行同一份線程函數(shù),所以這個(gè)線程函數(shù)就絕對(duì)會(huì)處于被重入的狀態(tài),也就絕對(duì)會(huì)被多個(gè)線程執(zhí)行!今天我們假設(shè)只有一個(gè)CPU(CPU就是核心,處理器芯片會(huì)集成多個(gè)核心)在調(diào)度當(dāng)前進(jìn)程中的線程,那么線程是CPU調(diào)度的基本單位,所以也就會(huì)出現(xiàn)一個(gè)線程可能執(zhí)行一半的時(shí)候被切換下去了,并且該線程的上下文被保存起來,然后CPU又去調(diào)度進(jìn)程中的另一個(gè)線程。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
3.
在知道上面的原理之后,還需要知道usleep的作用,當(dāng)usleep放到if分支語句的第一行時(shí),票數(shù)就出現(xiàn)了問題,出現(xiàn)了負(fù)數(shù),主要是因?yàn)閡sleep可以將線程暫時(shí)阻塞,那么CPU就會(huì)把他切換下去,轉(zhuǎn)而執(zhí)行其他線程,但需要注意的是,如果被切換的線程重新調(diào)度上來時(shí),還會(huì)從上次他執(zhí)行后的語句繼續(xù)向下運(yùn)行。
所以會(huì)出現(xiàn)多個(gè)線程同時(shí)進(jìn)入到分支判斷語句,然后去阻塞等待的情況,假設(shè)tickets已經(jīng)變成了1,然后其余的線程此時(shí)都被調(diào)度上來了,他們都開始執(zhí)行tickets- -,- -之后不滿足循環(huán)條件線程才會(huì)退出,那么如果我們創(chuàng)建出了4個(gè)線程,就會(huì)有3個(gè)線程在票數(shù)已經(jīng)為0的情況下繼續(xù)減減,所以就會(huì)出現(xiàn)票數(shù)為負(fù)數(shù)的情況。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

4.
而我們能夠復(fù)現(xiàn)出問題其實(shí)主要靠的是usleep和邏輯判斷與tickets- -分開,那么線程就有可能在執(zhí)行if邏輯判斷之后,還沒有執(zhí)行tickets- -之前就被切換下去了,而多個(gè)線程都出現(xiàn)這樣的情況時(shí),他們都被重新調(diào)度時(shí),重新加載自己的上下文數(shù)據(jù)時(shí),繼續(xù)向后執(zhí)行,但此時(shí)tickets已經(jīng)沒有了,共享資源tickets在多線程訪問時(shí)就會(huì)出現(xiàn)數(shù)據(jù)不安全的問題。

5.
我們上面是將邏輯判斷和tickets- -分開了,那是不是只要?jiǎng)e分開,就不會(huì)出現(xiàn)問題呢?
答案并不是這樣的,還是會(huì)出現(xiàn)問題的,只不過我們復(fù)現(xiàn)出這樣的問題需要靠概率而已,所以并不是那么好復(fù)現(xiàn)。但我們只要知道原理就可以,下面再來分析一下只有tickets- -這一步的情況下,是否會(huì)出現(xiàn)問題呢?
我舉了兩個(gè)線程同時(shí)循環(huán)執(zhí)行票數(shù)-1的例子。如果真要說到底,這些由于多線程操作共享資源而產(chǎn)生的問題,本質(zhì)原因只有一個(gè),他們可能在運(yùn)行的一半被切換走了,連同他自己的上下文結(jié)構(gòu),而被切換走的同時(shí),其他調(diào)度上來的線程依舊可以訪問這個(gè)共享資源,但是被切換下去的線程不知道啊!沒人告訴我??!我和我的上下文就等著被CPU重新調(diào)度回去呢!但等我回來的時(shí)候,天都已經(jīng)大變樣了!我還啥都不知道,繼續(xù)傻傻的操作共享變量,此時(shí)就出現(xiàn)共享資源數(shù)據(jù)不一致的問題了。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.提出解決方案:加鎖(局部和靜態(tài)鎖的兩種初始化/銷毀方案)

2.1 對(duì)于鎖的初步理解和實(shí)現(xiàn)

1.
那該如何解決上面的問題呢?多個(gè)執(zhí)行流操作共享資源時(shí),發(fā)生了數(shù)據(jù)不一致問題。
解決上面的問題實(shí)際要通過加鎖來實(shí)現(xiàn),但在談?wù)摷渔i的話題之前,我們需要來重新看待幾個(gè)概念。
多個(gè)執(zhí)行流總是能夠共享許多資源,但在加鎖保護(hù)后的共享資源我們稱為臨界資源。
而多個(gè)執(zhí)行流執(zhí)行的函數(shù)體內(nèi)部,對(duì)臨界資源進(jìn)行操作的代碼稱為臨界區(qū),需要注意的是臨界區(qū)不是整個(gè)函數(shù)體內(nèi)部的代碼,而是指對(duì)共享資源進(jìn)行操作的代碼稱為臨界區(qū)。
如果我們想讓多個(gè)執(zhí)行流串行的訪問臨界資源,而不是并發(fā)或并行的訪問臨界資源,這樣的線程調(diào)度方案就是互斥式的訪問臨界資源?。ù芯褪侵钢灰粋€(gè)線程開始執(zhí)行這個(gè)任務(wù),那么他就不能中斷,必須得等這個(gè)線程執(zhí)行完這個(gè)任務(wù),你才能切換其他線程執(zhí)行其他的任務(wù),這個(gè)概念等會(huì)講完鎖之后大家就明白什么是互斥了)
當(dāng)線程在執(zhí)行一個(gè)對(duì)資源訪問的操作時(shí),要么做了這個(gè)操作,要么沒有做這個(gè)操作,只要兩種狀態(tài),不會(huì)出現(xiàn)做了一半這樣的狀態(tài),我們稱這樣的操作是原子性的。(就比如你媽讓你寫作業(yè),你要么給我把作業(yè)寫完了再出去玩,要么就一個(gè)字也別寫給我滾出家門,就這兩種狀態(tài),不會(huì)出現(xiàn)你寫了一半,然后你媽讓你出去玩的這種情況,這樣也是原子性)

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.
有了上面四組概念的稍稍鋪墊之后,我們來談?wù)勅绾螌?duì)共享資源進(jìn)行加鎖和解鎖,首先鎖實(shí)際就是一種數(shù)據(jù)類型,這個(gè)鎖就像我們平常定義出來的變量或是對(duì)象一樣,只不過這個(gè)鎖的類型是系統(tǒng)給我們封裝好的一種類型,進(jìn)行重定義后為pthread_mutex_t。變量或?qū)ο笤谏臅r(shí)候也是可以初始化的,變量初始化后,就是變量的定義,而不是聲明了。變量和對(duì)象也都有自己的銷毀方案,內(nèi)置類型的變量銷毀時(shí),操作系統(tǒng)會(huì)自動(dòng)回收其資源,而自定義對(duì)象銷毀時(shí),操作系統(tǒng)會(huì)調(diào)用其析構(gòu)函數(shù)進(jìn)行資源的回收。
鎖同樣也是如此,鎖也有自己的初始化和銷毀方案,如果你定義的是一把局部鎖,就需要用pthread_mutex_init()和pthread_mutex_destroy()來進(jìn)行初始化和銷毀,如果你定義的是一把全局鎖或靜態(tài)所,則不需要用init初始化和destroy銷毀,直接用PTHREAD_MUTEX_INITIALIZER進(jìn)行初始化即可,他有自己的初始化和銷毀方案,我們無須關(guān)心靜態(tài)或全局鎖如何銷毀。
定義好鎖之后,我們就可以對(duì)某一段代碼進(jìn)行加鎖和解鎖,加鎖與解鎖意味著,這段代碼不是一般的代碼,只有申請(qǐng)到鎖,持有鎖的線程才能訪問這段代碼,加鎖和解鎖之間的代碼可以稱為臨界區(qū),因?yàn)橄胍L問這段空間必須有鎖才可以訪問。pthread_mutex_lock實(shí)際就是申請(qǐng)鎖的代碼和臨界區(qū)的入口,如果你申請(qǐng)鎖成功了,那么你就可以進(jìn)入臨界區(qū)訪問臨界資源,如果你并沒有申請(qǐng)成功,比如當(dāng)前這把鎖已經(jīng)被別的線程申請(qǐng)到并持有了,其他線程正持有鎖在臨界區(qū)訪問著呢,那么你就無法進(jìn)入臨界區(qū),因?yàn)槟悴]有持有鎖,必須得在pthread_mutex_lock這個(gè)接口外面等著,直到你申請(qǐng)到鎖之后,你才能進(jìn)入臨界區(qū)訪問臨界資源,這樣的線程訪問實(shí)際就是互斥,指的是當(dāng)一個(gè)線程正在持有鎖訪問臨界區(qū)的時(shí)候,其他線程無法進(jìn)入臨界區(qū),直到持有鎖的線程釋放鎖之后才會(huì)有可能進(jìn)入臨界區(qū),注意是有可能,因?yàn)楫?dāng)線程釋放鎖之后,這把鎖還需要被競爭,哪個(gè)線程競爭到這把鎖,哪個(gè)線程才能持有鎖的訪問臨界資源!

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

3.
上面談?wù)撏赕i的初始化和銷毀,以及如何加鎖和解鎖之后,我們來利用鎖解決上面出現(xiàn)的共享資源訪問不安全的問題。你不是由于多線程再進(jìn)行臨界資源訪問時(shí),可能由于線程切換什么的,導(dǎo)致非原子性式的訪問臨界資源嗎?那我不讓你這么干,我對(duì)這段臨界資源進(jìn)行加鎖,讓你當(dāng)前申請(qǐng)到鎖正在訪問臨界資源的線程,必須給我以原子性的訪問來訪問臨界資源,換句話說,你必須把訪問臨界資源的工作做完了,才可以,要么你不要訪問臨界資源,要么你訪問了臨界資源,就必須把臨界資源全部訪問完了,中間不能訪問一半就不訪問了!所以只要對(duì)臨界資源進(jìn)行加鎖后,臨界資源就變得安全了,因?yàn)闊o論什么線程想要訪問臨界資源,都必須以原子性的方式訪問完,這樣的話,就不會(huì)出現(xiàn)在訪問一半的時(shí)候,線程被切換下去了,其他線程被切換上來繼續(xù)訪問臨界資源了,而是說如果持有鎖的線程被切換下去了,這個(gè)線程會(huì)抱著他申請(qǐng)到的鎖被切換下去,此時(shí)其他線程如果被切換上來,想要訪問臨界資源,那也沒用,因?yàn)槟銢]有鎖??!持有鎖的線程被切換時(shí),是抱著鎖被切換的,那你現(xiàn)在既然訪問不了臨界區(qū),CPU無法繼續(xù)執(zhí)行代碼,那就只能等持有鎖的線程重新被切換上來時(shí),才能繼續(xù)開展臨界資源的訪問工作,這個(gè)工作必須且只能由申請(qǐng)到鎖的線程來完成,其他任何線程都無法完成這個(gè)工作!反過來說,這不就是原子性嗎?訪問臨界資源的工作只要被持有鎖的線程開始做了,哪怕他在做的過程中被切換下去了,也無須擔(dān)心,因?yàn)閯e的線程做不了這個(gè)工作,所以還是得等持有鎖的線程被切換上來的時(shí)候才能繼續(xù)做這個(gè)工作,那是不是這個(gè)工作只要開始做了,就一定會(huì)被做完呢?會(huì)不會(huì)出現(xiàn)做一半,停下來了不做了,讓別的線程在去訪問臨界資源的情況呢?當(dāng)然不會(huì)!這就是鎖帶來的作用。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

4.
如果在加鎖之后運(yùn)行代碼,實(shí)際可以發(fā)現(xiàn)他搶票的速度是要比沒加鎖之前慢的,原因也很簡單。我來給大家解釋一下,沒加鎖之前,線程之間是可以并發(fā)或并行執(zhí)行的,我先大概說一下并發(fā)和并行是什么,后面會(huì)詳細(xì)介紹這兩者的區(qū)別和概念,并發(fā)你可以簡單理解為,當(dāng)線程運(yùn)行一半被切換下去的時(shí)候,此時(shí)CPU還可以調(diào)度運(yùn)行其他線程,也就是說,如果多個(gè)線程在運(yùn)行的時(shí)候,每個(gè)線程都會(huì)被CPU跑一跑,那在一段時(shí)間內(nèi),所有的線程都可以被執(zhí)行到,并且推進(jìn)每個(gè)線程的執(zhí)行過程。而并行就是在多個(gè)核心上面同一時(shí)刻跑不同的線程,比如兩個(gè)同時(shí)訪問臨界資源的線程,在未加鎖的時(shí)候,可能出現(xiàn)多個(gè)核心同時(shí)執(zhí)行兩個(gè)線程的代碼,同時(shí)在訪問臨界資源,但實(shí)際這種情況并不常見,因?yàn)槲覀儗懗鰜淼拇a優(yōu)先級(jí)并沒有那么高,所以基本上都是在按照并發(fā)執(zhí)行的。
然后加鎖前是并發(fā)執(zhí)行的,也就是說在一個(gè)線程被切換下去的時(shí)候,其他- -tickets的線程還能夠被重新調(diào)度上來進(jìn)行票數(shù)的- -,那么總體上來說,票數(shù)就會(huì)被一直- -。
而加鎖之后就不是并發(fā)執(zhí)行的了,因?yàn)槲覀兩厦嬲f過,加鎖之后即使持有鎖的線程被切換下去,其他被調(diào)度到CPU上的線程也是無法進(jìn)行票數(shù)- -的,因?yàn)樗麄儧]有鎖,所以在持有鎖的線程被切換下去的這段時(shí)間里,票數(shù)不會(huì)改變,因?yàn)榫€程在串行的訪問臨界資源,什么是串行呢?就是一個(gè)線程訪問完之后,才能輪到另一個(gè)線程,就是我們前面說的,一個(gè)線程在完成他的工作之后,釋放完鎖之后,其他線程才有可能競爭到鎖,才有可能訪問臨界資源,這樣就是串行。
串行的執(zhí)行效率肯定要比并發(fā)執(zhí)行的效率底嘛,因?yàn)楫?dāng)多線程在執(zhí)行任務(wù)的時(shí)候,我們進(jìn)行并發(fā)執(zhí)行,為的就是當(dāng)前線程如果被切換下去了,那也沒啥事,因?yàn)槠渌徽{(diào)度上來的線程依舊可以執(zhí)行這個(gè)任務(wù)。你現(xiàn)在加鎖之后就會(huì)變成串行執(zhí)行了,那當(dāng)前持有鎖的線程被切換下去時(shí),其他被調(diào)度上來的線程是無法繼續(xù)執(zhí)行任務(wù)的,效率自然就會(huì)底一些。(效率底一點(diǎn)就底一點(diǎn)吧,畢竟現(xiàn)在共享資源就安全了嘛,下面運(yùn)行結(jié)果你也可以看到,沒有鎖的時(shí)候,票數(shù)就為負(fù)數(shù)了,這種情況用戶怎么可能容忍。)

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.2 局部和全局鎖的兩種加鎖方案的代碼實(shí)現(xiàn)

1.
如果定義局部鎖的話,我們肯定是想要將這把鎖傳給每個(gè)線程的,讓每個(gè)線程都用這把鎖來互斥式的訪問共享資源,以此來保證共享資源的安全性。并且我還想給每個(gè)線程帶上名字,這樣在打印結(jié)果上可以區(qū)分是哪個(gè)線程在進(jìn)行搶票。
所以我們是不是需要一個(gè)結(jié)構(gòu)體ThreadData來封裝一下鎖和線程名字呢?所以我們就定義出一個(gè)結(jié)構(gòu)體,把結(jié)構(gòu)體指針傳給線程,讓線程能夠使用鎖來訪問臨界資源!

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.
接下來我們還要看一下,加鎖之后的運(yùn)行現(xiàn)象。在沒有一次while循環(huán)之后的usleep(1000)時(shí),可以看到發(fā)生搶票的用戶,一段時(shí)間內(nèi)基本都會(huì)是一個(gè)用戶,比如打印結(jié)果中,如果是用戶1搶票,那大概用戶1要搶比較長的一段時(shí)間的票,然后才會(huì)換到其他用戶,這是為什么呢?
因?yàn)殒i只規(guī)定了線程必須互斥式的訪問臨界資源,但并沒有規(guī)定哪個(gè)線程先去執(zhí)行訪問臨界資源的操作!換句話說,只要你線程拿著鎖來訪問臨界資源,那我就同意你訪問,我管你是哪個(gè)線程呢,你有鎖就行,也就是說,你釋放完鎖之后,在重新競爭鎖的時(shí)候,如果你又能競爭到這把鎖,那你就一直拿著這個(gè)鎖來訪問就好了。你要是能一直競爭到鎖,那你就能一直來訪問臨界資源。
而下面現(xiàn)象我們其實(shí)可以看到,剛剛釋放完鎖的線程,在重新競爭鎖的時(shí)候,這個(gè)線程的競爭能力是比較強(qiáng)的,所以就會(huì)出現(xiàn)下面的現(xiàn)象,一個(gè)用戶搶票之后,大概還要搶很長時(shí)間的票。(同時(shí)其他線程就無法搶票,就只能眼巴巴的看著那個(gè)競爭能力強(qiáng)的線程一直在搶票,這樣的現(xiàn)象我們稱為饑餓狀態(tài),解決的方式實(shí)際是通過線程同步來解決的,這里先預(yù)熱一下,后面會(huì)詳細(xì)講的。)

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

3.
上面那種現(xiàn)象正確嗎?當(dāng)然是正確的!我這個(gè)線程競爭能力強(qiáng)嘛,我憑啥不能一直搶票呢?鎖只規(guī)定了我要互斥式的訪問臨界資源,又沒說必須是哪個(gè)線程先進(jìn)行或后進(jìn)行搶票,我就要一直搶票,你能把我怎么樣?
但是!上面的現(xiàn)象雖然是正確的,但是他不河貍!比如搶火車票,這個(gè)票一直被一個(gè)用戶搶,其他用戶一直都搶不著,那鐵路局咋賺錢呢?一個(gè)用戶的消費(fèi)咋能養(yǎng)活一個(gè)鐵路局呢?肯定得多個(gè)用戶消費(fèi)啊!
所以除了使用線程同步來解決之外,還可以通過usleep(1000)來解決,睡眠的多少不重要,只要讓線程在釋放完鎖之后,睡眠一會(huì)兒,將自己阻塞掛起(是否掛起是未知的,取決于OS)一會(huì)兒,阻塞掛起的時(shí)候,其他線程不就能競爭到鎖了嗎?那其他線程是不也可以進(jìn)行搶票了?就不用眼巴巴的看著競爭能力強(qiáng)的那個(gè)線程一直在搶票了!

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
下面是有usleep和沒有usleep的兩次結(jié)果對(duì)比,沒有usleep時(shí),一個(gè)線程可能會(huì)霸占搶票較長時(shí)間,有usleep時(shí),多個(gè)線程都可以協(xié)調(diào)的進(jìn)行搶票,不會(huì)出現(xiàn)一個(gè)線程持續(xù)霸占搶票的情況。
【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

4.
除了上面代碼使用局部鎖的實(shí)現(xiàn)方案外,我們還可以使用靜態(tài)鎖或全局鎖,局部的靜態(tài)鎖還是需要將鎖的地址傳給線程函數(shù),否則線程函數(shù)無法使用鎖,因?yàn)殒i是局部的嘛!如果是全局鎖,那就不需要將其地址傳給線程函數(shù)了,因?yàn)榫€程函數(shù)可以直接看到這把鎖,所以直接使用即可。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

3.根據(jù)代碼現(xiàn)象提出問題

3.1 如何看待鎖?

1.
完成上面對(duì)于共享資源訪問不安全問題的解決之后,我們來深入的理解一下鎖。
我們知道,共享資源在被多線程訪問時(shí),是不安全的,所以我們需要加鎖來保護(hù)共享資源。但是我們回過頭來想一想,鎖本身是不是共享資源呢?所有的線程都需要申請(qǐng)鎖和釋放鎖,那不就是在共同的訪問鎖這個(gè)資源嘛?所以鎖本身不就是共享資源嗎?那多個(gè)線程在訪問鎖這個(gè)共享資源的時(shí)候,鎖本身是不是需要被保護(hù)呢?當(dāng)然需要!其他的共享資源可以通過加鎖來進(jìn)行保護(hù),那鎖怎么辦呢?
實(shí)際上,加鎖和解鎖的過程是原子的!也就是說只要你申請(qǐng)了鎖,并且競爭能力恰好足夠,那么你就一定能夠拿到這個(gè)鎖,否則你就不會(huì)拿到這個(gè)鎖,不會(huì)說在申請(qǐng)鎖申請(qǐng)一半的時(shí)候,線程被切換下去了,其他線程去申請(qǐng)鎖了,不會(huì)出現(xiàn)這種中間態(tài)的情況!既然加鎖和解鎖的過程是原子的,那其實(shí)訪問鎖就是安全的!(但加鎖解鎖的過程為什么是原子的呢?我該如何理解呢?這個(gè)后面會(huì)說。)

地址空間中大部分的資源都是共享的,包括鎖本身這個(gè)共享資源
【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.
如果申請(qǐng)鎖成功了,那線程就會(huì)繼續(xù)向后執(zhí)行代碼,進(jìn)入臨界區(qū),訪問臨界資源。那如果申請(qǐng)鎖要是沒成功呢?或者說暫時(shí)申請(qǐng)不到鎖呢?執(zhí)行流又會(huì)怎么樣呢?
下面代碼中,線程函數(shù)內(nèi)部申請(qǐng)了兩次互斥鎖,這實(shí)際就會(huì)出問題了,可以看到代碼不會(huì)繼續(xù)運(yùn)行了,并且是進(jìn)程內(nèi)的所有線程都不會(huì)被調(diào)度,沒有一個(gè)線程能夠進(jìn)行搶票,我們通過ps -aL還可以看到線程確實(shí)都存在,但是都不會(huì)執(zhí)行代碼,并且ps -axj也可以看到當(dāng)前進(jìn)程變成了Sl+狀態(tài),也就是處于阻塞狀態(tài),而不是R運(yùn)行狀態(tài)!

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
3.
所以如果申請(qǐng)不到鎖,執(zhí)行流就會(huì)阻塞。
因?yàn)槟憔€程申請(qǐng)鎖的時(shí)候,鎖被別的線程拿走了,那你自然就無法申請(qǐng)到鎖,操作系統(tǒng)會(huì)將這樣的線程暫時(shí)處于休眠狀態(tài)。只有當(dāng)持有鎖的線程釋放鎖的時(shí)候,操作系統(tǒng)會(huì)執(zhí)行POSIX庫的代碼,重新喚醒休眠的線程,讓這個(gè)線程去競爭鎖,如果競爭到,那就持有鎖繼續(xù)向后運(yùn)行,如果競爭不到,那就繼續(xù)休眠。
那上面為什么會(huì)出問題呢?實(shí)際是因?yàn)椋?dāng)前線程已經(jīng)申請(qǐng)到鎖了,但是他又去申請(qǐng)鎖了,而這個(gè)鎖其實(shí)他自己正持有著呢,但是他又不知道自己持有鎖,因?yàn)槲覀冎饔^讓線程執(zhí)行了兩次申請(qǐng)鎖的語句,是我們讓他這么干的,他自己拿著鎖,然后他現(xiàn)在又要去申請(qǐng)鎖,但鎖實(shí)際已經(jīng)被持有了,那么當(dāng)前線程必然就會(huì)申請(qǐng)鎖失敗,也就是處于休眠狀態(tài),什么時(shí)候他才會(huì)喚醒呢?當(dāng)然是鎖被釋放的時(shí)候!當(dāng)鎖被釋放時(shí),操作系統(tǒng)才會(huì)喚醒當(dāng)前線程,但是鎖會(huì)釋放嗎?當(dāng)然是不會(huì)啦!因?yàn)槟阕约喊焰i拿著,你還等其他線程釋放鎖,人家其他線程又沒有鎖,你自己還運(yùn)行不到pthread_mutex_unlock這段代碼,也就是說你自己又不釋放鎖,你還讓沒有這個(gè)鎖的線程去釋放鎖,這不就是自己把自己給搞阻塞了嗎?這其實(shí)就是產(chǎn)生死鎖了,線程永遠(yuǎn)都無法等待鎖成功釋放,那么這個(gè)線程將永遠(yuǎn)處于阻塞狀態(tài),無法運(yùn)行,同樣其他線程道理也如此!
所以我們就可以看到,上面那么多線程全都阻塞了,每一個(gè)能跑的,其實(shí)就是因?yàn)榘l(fā)生死鎖問題了,所有的線程都無法申請(qǐng)到鎖,其中大部分的線程都是因?yàn)楦揪蜎]碰到鎖,一直想等鎖被釋放從而發(fā)生的休眠,而一個(gè)大傻線程是自己拿著鎖呢,但是還忘記自己拿著鎖了,要?jiǎng)e人把鎖還給他,而一直等待別人釋放鎖,從而產(chǎn)生的休眠問題!

4.
那該如何解決呢?兩種辦法,第一種就是通過pthread_mutex_trylock()來申請(qǐng)鎖,這個(gè)接口會(huì)試著進(jìn)行申請(qǐng)鎖,如果申請(qǐng)到鎖,那就繼續(xù)向后執(zhí)行代碼運(yùn)行即可。如果沒有申請(qǐng)到鎖,就會(huì)立馬出錯(cuò)返回!所以這個(gè)接口實(shí)際是一種非阻塞式的申請(qǐng)鎖的一種方式。從產(chǎn)生問題的原因角度解決了問題,你不是要阻塞式的申請(qǐng)鎖嗎?那我直接不阻塞不就得了?但其實(shí)這種解決方式是非常不好的,因?yàn)橐粋€(gè)線程出問題,整個(gè)進(jìn)程都會(huì)退出,你其他線程申請(qǐng)不到鎖就申請(qǐng)不到唄,但現(xiàn)在有一個(gè)線程申請(qǐng)到鎖了,并且互斥式的訪問臨界資源的呢,正訪問著呢,因?yàn)閯e的線程申請(qǐng)不到鎖,就把我當(dāng)前線程資源就回收了?而且所有的線程還都退出了!這合理嗎?當(dāng)然不合理!所以這樣的解決方式不好用,我們還是得用主流的lock和unlock來進(jìn)行鎖的申請(qǐng)和釋放!
所以對(duì)于lock申請(qǐng)到的鎖,還有另一種鎖的叫法,叫做掛起等待鎖
那該怎么解決呢?我所知道的實(shí)際并沒有很好的解決辦法,只能我們程序員小心再小心,千萬不要寫出死鎖的代碼,如果一旦寫出,那也要通過死鎖產(chǎn)生的問題,迅速補(bǔ)救代碼,檢查出死鎖產(chǎn)生的位置,進(jìn)行更改代碼!

實(shí)際上面總結(jié)下來也就一句話,誰持有鎖誰才能進(jìn)入臨界區(qū),你沒有鎖那就只能在臨界區(qū)外面乖乖的阻塞等待,等待鎖被釋放,然后你去競爭這把鎖,競爭到就拿著鎖進(jìn)入臨界區(qū)執(zhí)行代碼,競爭不到就老樣子,繼續(xù)乖乖的在臨界區(qū)外面阻塞等待!
【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
5.
上面我們已經(jīng)理解了臨界區(qū),臨界資源,串行執(zhí)行,未持有鎖線程的阻塞等待,以及互斥訪問這樣的概念。但在鎖這里,還有一個(gè)概念是原子性!我該如何真正的理解線程持有鎖的過程中原子性這樣的概念呢?
在談?wù)撜嬲斫饧渔i過程中的原子性概念之前,我們先來討論幾個(gè)問題。我這里就不說這些問題了,大家可以看我下面畫的圖。實(shí)際這些問題我們?cè)缇驮谏厦嬲f過了,無非就是未持有鎖的線程會(huì)阻塞等待式的等待鎖被釋放和持有鎖的線程在被調(diào)度切換時(shí),會(huì)拿著自己的鎖被切換下去,其他被重新調(diào)度到CPU上的線程依舊是無法申請(qǐng)到鎖的,因?yàn)殒i只有一把,而且是被剛剛切換下去的線程所持有的!所以被重新調(diào)度到CPU上的線程也沒啥用,因?yàn)樗麄儫o法繼續(xù)向后執(zhí)行代碼!這兩個(gè)話題其實(shí)上面都已經(jīng)說過了,我們這里就相當(dāng)于做一下復(fù)盤!

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
6.
那么!對(duì)于其他未持有鎖的線程而言,實(shí)際有意義的鎖的狀態(tài),無非就兩種!一種是申請(qǐng)鎖前,一種是釋放鎖后!申請(qǐng)鎖前,鎖還沒有被申請(qǐng)到,那么對(duì)于其他未持有鎖的線程來說,當(dāng)然是有意義的。釋放鎖后,鎖此時(shí)處于未被申請(qǐng)到的狀態(tài),那未持有鎖的線程當(dāng)然有可能競爭到這把鎖,所以這也是一種有意義的狀態(tài)!
而我們站在未持有鎖的線程角度來看的話,當(dāng)前持有鎖的線程不就是原子的嗎?他們看到的鎖只有在未申請(qǐng)前和持有鎖線程釋放鎖之后這兩種有意義的狀態(tài),那這就是原子的,不會(huì)出現(xiàn)中間態(tài)的情況。
所以,在未來使用鎖的時(shí)候,一定要保證臨界區(qū)的粒度非常小,因?yàn)榧渔i之后,線程會(huì)串行執(zhí)行,如果粒度非常大,那么執(zhí)行這段臨界區(qū)所耗費(fèi)的時(shí)間就越多,整體代碼運(yùn)行的效率自然就會(huì)降下來,因?yàn)槠溆喾桥R界區(qū)是并發(fā)或并行執(zhí)行,而臨界區(qū)是串行,所以整體效率會(huì)由于臨界區(qū)的執(zhí)行效率受較大影響,那么在平常加鎖和解鎖時(shí),我們就要保證臨界區(qū)的粒度較小,為此能夠讓程序整體的運(yùn)行效率依舊保持較高的狀態(tài)!

7.
談?wù)擃~外的幾個(gè)話題,我們說未持有鎖線程在等待釋放鎖期間會(huì)進(jìn)入阻塞狀態(tài),如果說具體一些的話,實(shí)際這些未持有鎖的線程會(huì)被放在互斥鎖對(duì)應(yīng)的等待隊(duì)列中,互斥鎖對(duì)象內(nèi)部維持了一個(gè)等待隊(duì)列,用于存放被該鎖阻塞的線程。
加鎖是程序員行為,如果要訪問共享資源,那么所有訪問該共享資源的線程都要加鎖,不能說有的線程加鎖有的線程不加鎖。比如現(xiàn)在有一批線程,他們要執(zhí)行兩個(gè)線程函數(shù),這兩個(gè)線程函數(shù)內(nèi)部都會(huì)訪問共享資源,但一個(gè)線程函數(shù)內(nèi)部對(duì)共享資源進(jìn)行加鎖,一個(gè)沒有加鎖,那么就會(huì)導(dǎo)致其中一批線程需要互斥式的串行訪問共享資源,而另一批線程則可以隨意并發(fā)式的訪問共享資源,這一定會(huì)出安全問題的,這算程序員寫出了bug,因?yàn)槟銓?duì)共享資源的保護(hù)不夠徹底,算你自己的問題!

3.2 如何理解加鎖和解鎖的本質(zhì)?(硬件層面和軟件層面的加鎖)

1.
在文章的較前部分,我們談到過單純的i++和++i的語句都不是原子的,因?yàn)檫@樣的語句實(shí)際還要至少對(duì)應(yīng)三條匯編語句,從內(nèi)存中讀取數(shù)據(jù),在寄存器中修改數(shù)據(jù),最后再將修改后的數(shù)據(jù)寫回內(nèi)存,所以++i和i++這樣的語句一定不是原子的,因?yàn)樗趫?zhí)行的時(shí)候是有中間態(tài)的,可能在執(zhí)行一半的時(shí)候由于某些原因被切換下去,這樣就會(huì)停下來。這種非原子性的操作就會(huì)導(dǎo)致數(shù)據(jù)不一致性的問題,也就是前面我們常談的共享資源訪問不安全的問題!隨之而來的解決方案就是我們所說的加鎖,對(duì)共享資源進(jìn)行互斥式的訪問,以保證其安全性。
而加鎖和解鎖的過程實(shí)際也是訪問共享資源鎖的過程,那么加鎖和解鎖是如何保證其訪問鎖的原子性呢?答案是通過一條匯編語句來實(shí)現(xiàn)。
為了實(shí)現(xiàn)互斥鎖的加鎖過程,大多數(shù)CPU架構(gòu)都提供了swap和exchange指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)進(jìn)行交換,因?yàn)橹挥幸粭l匯編指令,保證了其原子性。并且即便是多處理器平臺(tái),訪問內(nèi)存的總線周期也有先后,一個(gè)處理器上的交換指令執(zhí)行時(shí),另一個(gè)處理器的交換指令只能等待總線周期就緒后才能訪問。

2.
實(shí)際上除我們語言所說的一條匯編語句交換數(shù)據(jù),而保證的原子性外,在操作系統(tǒng)內(nèi)還有另一種硬件層面上的實(shí)現(xiàn)原子性的簡單做法。因?yàn)榫€程在執(zhí)行過程中,有可能出現(xiàn)線程執(zhí)行一半被切換了,那么線程完成任務(wù)就不是原子的了,所以我們能不能讓線程在執(zhí)行的時(shí)候,壓根就不能被切換,只要你線程上了CPU的賊船就不能下去,必須得等你完全執(zhí)行完代碼之后才可以被切換下去。
至于線程在執(zhí)行一半的時(shí)候被切換走,原因有很多,可能是時(shí)間片到了,來了更高優(yōu)先級(jí)的線程,線程由于訪問某些外設(shè)或自己的原因等等,需要進(jìn)行阻塞等待,這些情況下,都有可能在線程執(zhí)行一半的時(shí)候被切換下去!
所以在系統(tǒng)層面,我們只要禁止一切中斷,對(duì)線程的中斷不做任何響應(yīng),禁止中斷的總線做出任何響應(yīng),關(guān)閉外部中斷以達(dá)到線程不被切換下去的效果,從而實(shí)現(xiàn)訪問共享資源的原子性。
當(dāng)然這樣的方案比較偏底層,算是一個(gè)比較重量級(jí)的方案,在硬件層面實(shí)現(xiàn)這樣的方案的話,成本還是挺高的,除非線程要完成的工作優(yōu)先級(jí)特別高且必須是原子性的,我們才會(huì)這么做,否則一半情況下,不會(huì)采用這樣的方案來實(shí)現(xiàn)原子性。

3.
在談?wù)摷渔i過程的匯編代碼之前,我們先來談幾個(gè)共識(shí)性的話題,CPU內(nèi)寄存器只有一套,被所有的執(zhí)行流共享,并且CPU內(nèi)寄存器的內(nèi)容是每個(gè)執(zhí)行流都私有的,稱為運(yùn)行時(shí)的上下文??梢钥吹郊渔i的匯編語句就是將0放到al寄存器內(nèi)部,然后就是執(zhí)行只有一條的匯編語句xchgb,將al寄存器的內(nèi)容和物理內(nèi)存單元進(jìn)行數(shù)據(jù)交換,此時(shí)al寄存器內(nèi)容就會(huì)變?yōu)?,物理內(nèi)存中的mutex互斥量的值變?yōu)?,將物理內(nèi)存中mutex的1和al寄存器內(nèi)0進(jìn)行交換,我們可以形象化的表示為線程A把鎖拿走了,在拿走鎖之后,線程A有沒有可能被切換走呢?當(dāng)然有可能,但線程A在切換的時(shí)候,他是帶著自己的上下文數(shù)據(jù)被切換走的。
此時(shí)線程B被重新調(diào)度上來后,他也會(huì)先將0加載到自己上下文中的al寄存器內(nèi)部,然后再執(zhí)行xchgb匯編語句,但此時(shí)物理內(nèi)存的mutex是0,代表鎖已經(jīng)被申請(qǐng)了,所以交換以后,al寄存器內(nèi)部的值依舊是0,繼續(xù)判斷之后會(huì)進(jìn)入else分支語句,該線程就會(huì)由于等待鎖被持有鎖的線程釋放而處于掛起等待的狀態(tài)。
所以,只要線程A申請(qǐng)鎖成功了,即使線程A的運(yùn)行被中斷了,我們也不擔(dān)心,因?yàn)榻粨Q寄存器和內(nèi)存的匯編語句只有一條,這能保證加鎖過程,也就是申請(qǐng)鎖過程的原子性。并且在線程A被切走時(shí),線程A是持有鎖被切走的,那么即使其他線程此時(shí)被調(diào)度上來,他們也一定無法申請(qǐng)到鎖,那就必須進(jìn)行阻塞等待!只有重新調(diào)度線程A,將線程A的上下文加載到寄存器內(nèi)部,此時(shí)al內(nèi)容就會(huì)變?yōu)?,則返回return 0代表申請(qǐng)鎖成功,線程A就可以持有鎖式的訪問臨界區(qū)。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

4.
上面說的加鎖過程是原子的,交換寄存器和mutex內(nèi)容僅由一條匯編語句來完成,而mutex是我們所說的共享資源,所以一條匯編語句保證了mutex操作的原子性。
而解鎖的過程也非常簡單,直接將1mov到mutex里面就完成了釋放鎖的過程,然后喚醒阻塞等待鎖的線程,讓他們現(xiàn)在去競爭鎖,因?yàn)殒i已經(jīng)被釋放了,所以同樣的,釋放鎖的匯編語句也只有一條,這也能保證釋放鎖過程的原子性!

3.3 RAII風(fēng)格的封裝設(shè)計(jì)鎖?(構(gòu)造函數(shù)加鎖,析構(gòu)函數(shù)解鎖)

1.
如果我們想簡單的封裝使用鎖,那我們?cè)撊绾卧O(shè)計(jì)呢?我們也想像之前封裝設(shè)計(jì)線程那樣搞出來C++式的面向?qū)ο蟀娴膭?chuàng)建線程和銷毀線程。
實(shí)際實(shí)現(xiàn)起來也很簡單,無非就是對(duì)原生的申請(qǐng)鎖,加鎖,解鎖接口的封裝!我們先定義一個(gè)互斥量的類,類中實(shí)現(xiàn)構(gòu)造函數(shù)將鎖的地址進(jìn)行初始化,然后定義出加鎖和解鎖的兩個(gè)接口,這樣就可以定義出來一個(gè)內(nèi)部能夠進(jìn)行加鎖和解鎖的類。
然后我們?cè)偌右粚臃庋b,實(shí)現(xiàn)出RAII( Resource Acquisition Is Initialization)風(fēng)格的加鎖,即為構(gòu)造函數(shù)處進(jìn)行加鎖,析構(gòu)函數(shù)處進(jìn)行解鎖!
至于鎖的初始化和銷毀方案,是類外面的事情,使用時(shí)需要自己先初始化好一把鎖,確定初始化和銷毀的方案,然后利用Mutex.hpp這個(gè)小組件來進(jìn)行加鎖和解鎖的過程!

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
2.
在這里補(bǔ)充一個(gè)知識(shí)點(diǎn),對(duì)象的生命周期是隨代碼塊兒的,也就是說,當(dāng)對(duì)象離開代碼塊兒的時(shí)候,會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù),例如下面搶票代碼中,我們不想把usleep(1000)也放入到臨界區(qū),因?yàn)榧渔i之后的代碼都屬于臨界區(qū)了,只有對(duì)象銷毀時(shí)才會(huì)發(fā)生解鎖,所以我們就可以利用代碼塊兒來實(shí)現(xiàn)臨界區(qū)的范圍管控。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

沒有代碼塊就會(huì)出現(xiàn),剛釋放完鎖的線程競爭能力強(qiáng),持續(xù)霸占搶票,導(dǎo)致其他線程出現(xiàn)饑餓問題,有代碼塊也就是前面我們說過的,在釋放完鎖之后,讓剛剛持有鎖的線程停一會(huì)兒,讓其他線程也能競爭到鎖,也能進(jìn)行搶票!
【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

我之前并不知道這個(gè)知識(shí)點(diǎn),或者說知道的并沒有那么清楚,像上面那種代碼塊兒的使用方法我倒是沒有見過,所以特地跑到vs上面驗(yàn)證了一下,下面是驗(yàn)證結(jié)果,事實(shí)確實(shí)如上面所說那樣。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

4.可重入與線程安全

1.
在多線程并發(fā)執(zhí)行代碼,同時(shí)訪問共享資源的時(shí)候,如果某一個(gè)共享資源由于多線程訪問,發(fā)生了數(shù)據(jù)不一致,共享資源不安全,并且導(dǎo)致其他線程運(yùn)行出問題了,那么這種情況就是線程不安全的。尤其對(duì)于沒有鎖保護(hù)的共享資源的多線程訪問的代碼,很大概率出現(xiàn)線程不安全的情況。
而什么是可重入呢?這個(gè)話題并不陌生,我們之前談?wù)撨M(jìn)程信號(hào)的時(shí)候,進(jìn)程可能由于收到信號(hào),并且在陷入內(nèi)核時(shí)檢測(cè)到信號(hào),跳轉(zhuǎn)到handler方法執(zhí)行信號(hào)處理函數(shù),信號(hào)處理函數(shù)中可能會(huì)出現(xiàn)和main執(zhí)行流中執(zhí)行相同的函數(shù)體,例如當(dāng)時(shí)我們所說的鏈表的push_back在main和handler中同時(shí)執(zhí)行,可能會(huì)導(dǎo)致某些未知錯(cuò)誤的產(chǎn)生,如果出現(xiàn)了問題,那么我們稱這個(gè)函數(shù)是不可重入函數(shù),如果沒有出現(xiàn)問題這個(gè)函數(shù)就是可重入函數(shù)。值得注意的是,不可重入函數(shù)說的是這個(gè)函數(shù)的屬性,而不是說這個(gè)函數(shù)叫做不可重入函數(shù),那么他就一定不能被執(zhí)行流所重入,只是說,他如果被執(zhí)行流重入,極大概率是要出問題的。

2.
下面是一些線程安全和不安全,函數(shù)可重入和不可重入的話題,實(shí)際就是混一堆概念,寫代碼的時(shí)候根本用不到,也就是現(xiàn)在在這里說一下而已。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

3.
一句話,可重入函數(shù)是線程安全的充分不必要條件,線程函數(shù)如果是可重入的,那么就一定是線程安全的,反過來是不一定的。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

5.死鎖

5.1 死鎖概念

1.
死鎖是指一個(gè)進(jìn)程中的各個(gè)線程,都持有著鎖,但同時(shí)又去申請(qǐng)其他線程的鎖,而每個(gè)線程持有的鎖都是占有不會(huì)釋放的,所以大家都會(huì)等著,等對(duì)方先釋放鎖,但是呢,大家又都不釋放鎖,全都占有著鎖,所以大家就會(huì)處于一種永久等待的狀態(tài),也就是永久性的阻塞狀態(tài),所有執(zhí)行流都不會(huì)被運(yùn)行,這樣的問題就是死鎖!
之前搶票的代碼中,多個(gè)線程使用的是同一把鎖,未來有些場景一定是要使用多把鎖的,在多把鎖的情況下,如果某些線程持有鎖不釋放,還要去申請(qǐng)其他線程正持有的鎖,而每個(gè)線程都是這樣的狀態(tài),那就是死鎖問題。

2.
一把鎖有可能造成死鎖問題嗎?當(dāng)然是有可能的,前面我們談到過這個(gè)問題,一個(gè)線程已經(jīng)持有鎖了,但他又去等待這個(gè)鎖釋放,但這個(gè)鎖現(xiàn)在釋放不了,那他自己就會(huì)持有鎖式的阻塞等待。其實(shí)就是一個(gè)人騎著毛驢找毛驢,那他最后能找到毛驢嗎?當(dāng)然是找不到的!

3.
下面來談一下產(chǎn)生死鎖的邏輯鏈條,大家看一下就好,我們談?wù)摰闹攸c(diǎn)還是產(chǎn)生死鎖的四個(gè)必要條件,這里只是對(duì)死鎖產(chǎn)生做一個(gè)解釋而已。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

5.2 產(chǎn)生死鎖的四個(gè)必要條件

1.
互斥條件:一個(gè)資源每次只能被一個(gè)執(zhí)行流使用,互斥其實(shí)就是加鎖之后線程的串行執(zhí)行。
請(qǐng)求與保持條件:一個(gè)執(zhí)行流由于請(qǐng)求資源而阻塞時(shí),對(duì)自己已經(jīng)獲得的資源保持不放。說白了就是我自己的東西不釋放,我還要你的東西,你不給我就一直等,等到你給我為止。
不剝奪條件:一個(gè)線程在未使用完自己獲得的資源之前,是不能夠強(qiáng)行剝奪其他線程的資源的。說白了就是你先在還有資源呢,你想要?jiǎng)e人的自由你就得等,不能強(qiáng)行剝奪!當(dāng)你使用完自己的資源后,你可以去等待申請(qǐng)別人的資源??傊褪遣荒軓?qiáng)行剝奪其他線程的資源,想要就必須阻塞等待別人釋放資源才可以。
循環(huán)等待條件:若干個(gè)執(zhí)行流之間,形成一種頭尾相接的互相等待對(duì)方資源的關(guān)系。我們也稱這樣的現(xiàn)象為環(huán)路等待。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.
破壞死鎖實(shí)際就是破壞死鎖的四個(gè)條件其中之一,只要破壞一個(gè)條件,死鎖就無法產(chǎn)生。
第一個(gè)互斥是鎖的特性,我們無法改變。
在申請(qǐng)第二把鎖的時(shí)候,如果申請(qǐng)暫時(shí)不成功,那就不去阻塞等待該鎖被釋放,而是直接出錯(cuò)返回,這樣就破壞了保持的條件,也就是說如果請(qǐng)求不成功,也不保持自己的資源不釋放了,而是直接釋放資源,出錯(cuò)返回,這樣也能避免死鎖。例如使用pthread_mutex_trylock來申請(qǐng)鎖。
我們可以設(shè)定一個(gè)競爭策略,例如優(yōu)先級(jí)較高的線程可以剝奪優(yōu)先級(jí)較低線程的資源,也就是可以搶過來,直接把優(yōu)先級(jí)較低線程的鎖搶過來。所以判斷能否剝奪資源時(shí),我們通過優(yōu)先級(jí)的高低就可以判斷。
因?yàn)樯暾?qǐng)鎖的順序而導(dǎo)致線程出現(xiàn)了環(huán)路等待問題,所以我們就讓他們申請(qǐng)鎖的順序保持一致,不要產(chǎn)生環(huán)路等待的問題。例如:假設(shè)訪問臨界資源需要持有AB兩把鎖,那么讓所有線程申請(qǐng)鎖的順序都是先申請(qǐng)A鎖再申請(qǐng)B鎖,這樣的話,申請(qǐng)A鎖成功的線程一定能申請(qǐng)到B鎖,那么該線程就可以拿著這兩把鎖去訪問臨界區(qū),而其他線程由于連A鎖都申請(qǐng)不到,更別說申請(qǐng)B鎖了,所以他們就只能等待持有鎖線程釋放A鎖,這樣的好處就是不會(huì)產(chǎn)生死鎖問題。如果你不這么做,那一定會(huì)導(dǎo)致死鎖問題的產(chǎn)生,例如一個(gè)線程先申請(qǐng)A鎖再申請(qǐng)B鎖,另一個(gè)線程先申請(qǐng)B鎖再申請(qǐng)A鎖,那么就會(huì)出現(xiàn)第一個(gè)線程一直等后一個(gè)線程釋放B鎖,而后一個(gè)線程一直在等第一個(gè)線程釋放A鎖,而每個(gè)線程都是請(qǐng)求與保持的,所以最終結(jié)果就是,兩個(gè)線程都一直處于永久阻塞等待的狀態(tài),此時(shí)就產(chǎn)生死鎖問題。(這種解決方案還是很不錯(cuò)的,讓所有線程申請(qǐng)鎖的順序保持一致?。?/mark>

3.
那么如何避免死鎖呢?我們可以通過下面的幾種方式來避免死鎖,這些是程序員在寫代碼上需要注意的一些細(xì)節(jié)。
例如資源一次性分配這樣的細(xì)節(jié),如果一個(gè)接口里面大量的申請(qǐng)了空間資源,那么就提前將這些資源申請(qǐng)好,而不是在寫代碼的途中進(jìn)行資源申請(qǐng),因?yàn)樵诙嗑€程的環(huán)境下,多個(gè)執(zhí)行流,還有鎖的情況,你在代碼中進(jìn)行資源申請(qǐng),是有可能出現(xiàn)問題的,如果代碼量巨大,那出現(xiàn)的問題真是能頭疼死人!同樣加鎖的條件也會(huì)變得非常復(fù)雜。
所以在多線程環(huán)境下,強(qiáng)烈建議要將資源進(jìn)行一次性分配,如果你不這么做,也沒關(guān)系,因?yàn)榇a出錯(cuò)之后,代碼會(huì)教你做人的。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

4.
除上面需要注意的避免產(chǎn)生死鎖的代碼編寫之外,還有兩個(gè)避免死鎖產(chǎn)生的算法需要說一下。
首先提一個(gè)問題,一個(gè)線程申請(qǐng)的鎖,另一個(gè)線程可以釋放這個(gè)鎖嗎?當(dāng)然是可以的!釋放鎖不就是調(diào)用一下unlock接口嘛,哪個(gè)線程不能做這個(gè)工作啊,只要把對(duì)應(yīng)鎖的地址傳給任意一個(gè)線程,該線程都可以通過調(diào)用unlock接口來釋放鎖。所以一種死鎖檢測(cè)的算法思想就是定義一個(gè)類,類里面定義計(jì)數(shù)器,這個(gè)計(jì)數(shù)器衡量的是每個(gè)線程是否運(yùn)行,只要線程運(yùn)行,那么這個(gè)計(jì)數(shù)器就會(huì)一直++,然后可以用另一個(gè)監(jiān)控線程盯著這個(gè)計(jì)數(shù)器,一旦計(jì)數(shù)器長時(shí)間不變化,就有可能產(chǎn)生死鎖,此時(shí)監(jiān)控線程負(fù)責(zé)將鎖unlock釋放,通過直接釋放鎖的方式來避免產(chǎn)生死鎖。

銀行家算法(了解)

5.
即使教材上面對(duì)于死鎖的解決方案說的非常詳細(xì),但實(shí)際在工程中能不用鎖盡量不要用鎖,如果非常必須用鎖來解決問題,那也要盡量少的鎖來解決問題。因?yàn)檫@個(gè)鎖和C++的模板一樣,水很深!我們并不能因?yàn)槲覀冋趯W(xué)這個(gè)東西,那這個(gè)東西就一定是重要的,或者是實(shí)際中使用率較高的,這不是絕對(duì)的。

二、線程同步+生產(chǎn)消費(fèi)模型

1.通過條件變量拋出線程同步的話題

1.
我們前面就說過,在搶票邏輯中,剛釋放完鎖的線程由于競爭能力比較強(qiáng),導(dǎo)致其他線程無法申請(qǐng)到鎖,那么長時(shí)間其他線程都無法申請(qǐng)到鎖,只能阻塞等待著,這樣的線程處于饑餓狀態(tài)!
我們可以舉一個(gè)例子來理解條件變量是如何實(shí)現(xiàn)線程同步的。
假設(shè)現(xiàn)在學(xué)校開了一間學(xué)霸vip自習(xí)室,學(xué)校規(guī)定這間自習(xí)室一次只能進(jìn)去一個(gè)人上自習(xí),自習(xí)室門口掛著一把鑰匙,誰來的早先拿到這把鑰匙,就可以打開門進(jìn)入自習(xí)室學(xué)習(xí),并且進(jìn)入自習(xí)室之后,把門一反鎖,其他人誰都不能進(jìn)來。然后你第二天準(zhǔn)備去學(xué)習(xí)了,卷的不行,直接凌晨三點(diǎn)就跑過來,拿著鑰匙進(jìn)入自習(xí)室上自習(xí)了,然后卷了3小時(shí)之后,你想出來上個(gè)廁所,一打開門發(fā)現(xiàn)外面站的一堆人,都在嘰嘰喳喳的討論誰先來的,怎么來的這么早?這么卷?然后你怕自己等會(huì)兒把鑰匙放到墻上之后,上完廁所回來之后有人拿著鑰匙進(jìn)入了自習(xí)室,你就又卷不了了,所以你把鑰匙揣兜里,拿著鑰匙去上廁所了,其他人當(dāng)然進(jìn)入不了自習(xí)室,因?yàn)槟隳弥€匙去上廁所了。等你回來的時(shí)候,你又打開門,又來里面上了3小時(shí)自習(xí),你感覺自己餓的不行了,在不吃飯就餓死在里面了,所以你打開門,準(zhǔn)備出去吃飯了,然后突然你自己感覺負(fù)罪感直接拉滿,我凌晨3點(diǎn)好不容易搶到自習(xí)室,現(xiàn)在離開是不太虧了,所以你又打開自習(xí)室回去上自習(xí)去了,別人當(dāng)然競爭不過你呀!因?yàn)殍€匙一直都在你兜里,你出來之后把鑰匙放到墻上,你發(fā)現(xiàn)有點(diǎn)負(fù)罪感,你又拿起來鑰匙回去上自習(xí),因?yàn)槟汶x鑰匙最近,所以你的競爭能力最強(qiáng)。結(jié)果你來自習(xí)室上了1分鐘自習(xí)又出來了,然后又負(fù)罪的不行,又回去了,周而復(fù)始的這么干,結(jié)果別人連自習(xí)室長啥樣都沒見到。
像這樣由于長時(shí)間無法得到鎖的線程,沒辦法進(jìn)入臨界區(qū)訪問臨界資源,我們稱這樣的線程處于饑餓狀態(tài)!

2.
所以學(xué)校推出了新政策,所有剛剛從自習(xí)室出來的人,都必須回到隊(duì)列的尾部重新排隊(duì)等待進(jìn)入自習(xí)室,這樣的話,其他人也就可以拿到鑰匙進(jìn)入自習(xí)室了。
所以,在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序來訪問臨界資源,從而有效避免其他線程的饑餓問題,這就叫做線程同步!

2.生產(chǎn)消費(fèi)模型的概念理解(321原則)

1.
上面我們已經(jīng)初步理解了條件變量帶來的作用,那就是讓互斥訪問的線程能夠?qū)崿F(xiàn)同步,有效避免其他線程的饑餓問題,但在真正學(xué)習(xí)使用條件變量之前,我們還需要再來談?wù)撘粋€(gè)模型,叫做生產(chǎn)消費(fèi)模型,在談?wù)撏晟a(chǎn)消費(fèi)模型之后,我們?cè)趤硎褂靡幌聴l件變量,然后基于條件變量+生產(chǎn)消費(fèi)模型實(shí)現(xiàn)出一個(gè)基于阻塞隊(duì)列式的生產(chǎn)消費(fèi)模型代碼。

2.
實(shí)際生活中,我們作為消費(fèi)者,一般都會(huì)去超市這樣的地方去購買產(chǎn)品,而不是去生產(chǎn)者那里購買產(chǎn)品,因?yàn)楣┴浬桃话悴涣闶郛a(chǎn)品,他們都會(huì)統(tǒng)一將大量的商品供貨到超市,然后我們消費(fèi)者從超市這樣的交易場所中購買產(chǎn)品。
而當(dāng)我們?cè)谫徺I產(chǎn)品的時(shí)候,生產(chǎn)者在做什么呢?生產(chǎn)者可能正在生產(chǎn)商品呢,或者正在放假呢,也可能正在干著別的事情,所以生產(chǎn)和消費(fèi)的過程互相并不怎么影響,這就實(shí)現(xiàn)了生產(chǎn)者和消費(fèi)者之間的解耦。
而超市充當(dāng)著一個(gè)什么樣的角色呢?比如當(dāng)放假期間,消費(fèi)爆棚的季節(jié)中,來超市購買東西的人就會(huì)非常的多,所以就容易出現(xiàn)供不應(yīng)求的情況,但超市一般也會(huì)有對(duì)策,因?yàn)槌械膫}庫中都會(huì)預(yù)先屯一批貨,所以在消費(fèi)爆棚的時(shí)間段內(nèi),超市也不用擔(dān)心沒有貨賣的情況。而當(dāng)工作期間,大家由于忙著通過勞動(dòng)來換取報(bào)酬,可能來消費(fèi)的人就會(huì)比較少,商品流量也會(huì)比較低,那此時(shí)供貨商如果還是給超市供大量的貨呢?雖然超市可能最近確實(shí)賣不出去東西,但是超市還是可以把供貨商的商品先存儲(chǔ)到倉庫中,以備在消費(fèi)爆棚的季節(jié)時(shí),能夠應(yīng)對(duì)大量消費(fèi)的場景。所以超市其實(shí)就是充當(dāng)一個(gè)緩沖區(qū)的角色,在計(jì)算機(jī)中充當(dāng)?shù)木褪菙?shù)據(jù)緩沖區(qū)的角色。
而計(jì)算機(jī)中哪些場景是強(qiáng)耦合的呢?其實(shí)函數(shù)調(diào)用就是強(qiáng)耦合的一個(gè)場景,例如當(dāng)main調(diào)用func的時(shí)候,func在執(zhí)行代碼的時(shí)候,main在做什么呢?main什么都做不了,他只能等待func調(diào)用完畢返回之后,main才能繼續(xù)向后執(zhí)行代碼,所以我們稱main和func之間就是一種強(qiáng)耦合的關(guān)系,而上面所說的生產(chǎn)者和消費(fèi)者并不是一種強(qiáng)耦合的關(guān)系。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
3.
如果深度挖掘一下生產(chǎn)消費(fèi)模型,超市其實(shí)就是典型的共享資源,因?yàn)樯a(chǎn)者和消費(fèi)者都要訪問超市,所以對(duì)于超市這個(gè)共享資源,他在被訪問的時(shí)候,也是需要被保護(hù)起來的,而保護(hù)其實(shí)就是通過加鎖來實(shí)現(xiàn)互斥式的訪問共享資源,從而保證安全性。
在只有一份超市共享資源的情況下,生產(chǎn)和生產(chǎn),消費(fèi)和消費(fèi),以及生產(chǎn)和消費(fèi)都需要進(jìn)行串行的訪問共享資源。但為了提高效率我們搞出了同步這樣的關(guān)系,因?yàn)橛锌赡芟M(fèi)者一直霸占著鎖,一直在那里消費(fèi),但實(shí)際超市已經(jīng)沒有物資了,此時(shí)消費(fèi)者由于競爭能力過強(qiáng),也會(huì)造成不合理的問題,因?yàn)橄M(fèi)者消費(fèi)過多之后,應(yīng)該輪到生產(chǎn)者來生產(chǎn)了,所以對(duì)于生產(chǎn)者和消費(fèi)者之間僅僅只有互斥關(guān)系是不夠的,還需要有同步關(guān)系。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
4.
從生產(chǎn)消費(fèi)模型中可以提取出來一個(gè)321原則。即為3種關(guān)系,兩個(gè)角色,1個(gè)交易場所。對(duì)應(yīng)的其實(shí)是消費(fèi)線程和消費(fèi)線程的關(guān)系,消費(fèi)線程和生產(chǎn)線程的關(guān)系,生產(chǎn)線程和生產(chǎn)線程的關(guān)系,交易場所就是阻塞隊(duì)列blockqueue。而實(shí)現(xiàn)線程同步就需要一個(gè)條件變量,比如生產(chǎn)者生產(chǎn)完之后,超市給消費(fèi)者打個(gè)電話,讓消費(fèi)者過來消費(fèi),消費(fèi)完之后,超市在給生產(chǎn)者打個(gè)電話,讓生產(chǎn)者來生產(chǎn),這樣就不會(huì)存在由于某一個(gè)線程競爭能力過強(qiáng),一直生產(chǎn)或一直消費(fèi)的情況產(chǎn)生,從而導(dǎo)致其他線程饑餓的問題。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
5.
所以總結(jié)一下生產(chǎn)消費(fèi)模型都有哪些好處。
a.他實(shí)現(xiàn)了生產(chǎn)和消費(fèi)的解耦,使他們之間并不互相影響。
b.支持生產(chǎn)和消費(fèi)一段時(shí)間的忙閑不均的問題。因?yàn)榫彌_區(qū)可以預(yù)留一部分?jǐn)?shù)據(jù),進(jìn)行數(shù)據(jù)的緩沖。
c.由于生產(chǎn)和消費(fèi)的互斥與同步關(guān)系,提升了生產(chǎn)消費(fèi)模型的效率

但我其實(shí)還有一個(gè)問題,生產(chǎn)和消費(fèi)是互斥的關(guān)系,那生產(chǎn)者生產(chǎn)的時(shí)候,消費(fèi)者就不能消費(fèi),因?yàn)楣蚕碣Y源需要被加鎖保護(hù),而鎖只有一把,所以每次只能有一個(gè)線程訪問這個(gè)共享資源,那你憑什么說生產(chǎn)消費(fèi)模型就高效了呢?這個(gè)問題很重要,后面講完阻塞隊(duì)列的代碼實(shí)現(xiàn)之后,要重點(diǎn)談一下這個(gè)問題!

3.條件變量實(shí)現(xiàn)線程同步的原理(條件變量內(nèi)部維護(hù)了線程的等待隊(duì)列,能wait線程也能wakeup線程)

1.
為了能夠讓多線程協(xié)同工作,就需要實(shí)現(xiàn)多線程的同步關(guān)系,為了維護(hù)同步關(guān)系,就需要引入條件變量。那條件變量是一個(gè)什么東西呢?他其實(shí)和互斥鎖一樣,都是一個(gè)數(shù)據(jù)類型定義出來的對(duì)象。初始化和銷毀方案和互斥鎖一模一樣。唯一不同的是,條件變量在使用時(shí)有兩個(gè)高頻使用的接口,一個(gè)是pthread_cond_wait,該函數(shù)的作用是將等待某一個(gè)具體鎖的線程放入條件變量的等待隊(duì)列中進(jìn)行等待,另一個(gè)是pthread_cond_signal,該函數(shù)的作用是喚醒條件變量中等待隊(duì)列的第一個(gè)等待線程,另一個(gè)用的不怎么高頻,但也偶爾會(huì)用一下的接口就是pthread_cond_broadcast,該函數(shù)將條件變量中的所有等待線程都會(huì)喚醒,讓所有線程重新回歸競爭鎖的狀態(tài)。而不是像signal那樣,喚醒cond隊(duì)列中任意一個(gè)阻塞等待鎖的線程。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.
除了之前我們舉的自習(xí)室的例子之外,下面又舉了一個(gè)面試官面試求職者的例子,其實(shí)說這么多例子就是為了讓大家感受到條件變量所帶來的作用,它能夠讓所有互斥訪問的線程都能夠按照某種順序進(jìn)入臨界區(qū),訪問臨界資源,這就是環(huán)境變量帶來的最大的作用。既能保證共享資源訪問的安全性,又能保證所有線程都可以拿到鎖去訪問共享資源,避免出現(xiàn)線程饑餓的問題。所以下面的例子大家看一下就好,如果你已經(jīng)深刻的認(rèn)識(shí)到條件變量帶來的好處和作用,以及他所實(shí)現(xiàn)的線程同步的話,你可以直接忽略這段文字,跳轉(zhuǎn)到下面條件變量實(shí)現(xiàn)同步的原理部分。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

3.
我們可以將條件變量理解為一個(gè)結(jié)構(gòu)體,它內(nèi)部會(huì)有一個(gè)字段專門表示當(dāng)前線程等待的鎖的使用情況,如果status有效,那么代表此時(shí)鎖也被釋放,還有一個(gè)字段是專門維護(hù)等待某一個(gè)鎖的線程隊(duì)列。當(dāng)status變?yōu)橛行У臅r(shí)候,我們可以調(diào)用pthread_cond_signal喚醒cond內(nèi)部的等待隊(duì)列中的某一個(gè)線程,將這個(gè)線程的上下文加載到CPU的寄存器上,并且這個(gè)線程會(huì)申請(qǐng)到上一個(gè)線程釋放的鎖,然后這個(gè)線程就可以拿著鎖互斥的去訪問臨界區(qū)了。
所以條件變量實(shí)現(xiàn)同步的根本原因就是通過wait和signal來實(shí)現(xiàn)的,比如某一個(gè)線程釋放完鎖了,那你這個(gè)線程就不要再給我繼續(xù)申請(qǐng)鎖了,因?yàn)槲乙獑拘裞ond的等待隊(duì)列中的線程了,他們還想要這把鎖呢,至于你,就去cond的等待隊(duì)列中等著就行了,等下次喚醒到你的時(shí)候,你才有資格重新申請(qǐng)鎖。所以通過條件變量等待和喚醒的這樣一種方式,成功實(shí)現(xiàn)了多個(gè)線程都能互斥式的訪問臨界區(qū),而不會(huì)出現(xiàn)某些線程無法申請(qǐng)到鎖而產(chǎn)生的饑餓問題。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

4.串行、并發(fā)、并行的概念

1.
接下來我要給大家介紹幾個(gè)概念,是關(guān)于串行、并發(fā)、并行的。單獨(dú)說這幾個(gè)概念實(shí)際并不難,但他們?cè)诂F(xiàn)代計(jì)算機(jī)中是如何被分配的,這樣的知識(shí)就比較珍貴了。另外需要說一點(diǎn)的是,網(wǎng)上有很多都喜歡把多核叫做多CPU,但是吧這么叫確實(shí)沒什么太大的錯(cuò)誤,因?yàn)橐粋€(gè)處理器芯片上集成了多個(gè)核心,每個(gè)核心都有自己獨(dú)立的存儲(chǔ)單元,控制單元,算術(shù)邏輯單元,所以每個(gè)核心都可以跑不同的任務(wù),從功能角度來講,確實(shí)可以叫做多CPU,但是也容易誤導(dǎo)萌新啊,就比如我這樣的,我以為是真的多CPU處理器呢,原來是大部分人的叫法不同而已。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.
實(shí)際我們的計(jì)算機(jī)在工作時(shí),是一定要進(jìn)行并發(fā)的,因?yàn)椴l(fā)能很好解決用戶同時(shí)想要運(yùn)行多個(gè)程序的需求,也就是我們所說的多任務(wù)處理,但同時(shí)也需要進(jìn)行并行。就比如上面圖中舉得例子,每個(gè)大核跑不同的程序,但同時(shí)某一個(gè)大核在跑程序時(shí),也可以時(shí)間片輪轉(zhuǎn)的去執(zhí)行另一個(gè)程序,所以并行和并發(fā)在計(jì)算中是同時(shí)存在的。
而并發(fā)一定要比并行效率高的前提是多任務(wù)情況,如果你站在多任務(wù)處理的角度去看待串行和并發(fā),你一定可以理解為什么并發(fā)效率要更高,因?yàn)榇性诰€程被切換下去或者等鎖被釋放的時(shí)候,這段時(shí)間CPU什么都做不了,那這段時(shí)間就會(huì)被白白浪費(fèi)掉,在多任務(wù)處理的情況下,效率一定就會(huì)下降。而對(duì)于并發(fā)來講,如果某個(gè)線程被切換下去或者他在等待鎖被釋放的時(shí)候,是完全沒有關(guān)系的,因?yàn)镃PU會(huì)調(diào)度運(yùn)行其他線程,所以被切換下去的線程在等待的時(shí)候,時(shí)間完全不會(huì)被浪費(fèi)掉,而是會(huì)被CPU利用起來去跑其他的線程。
我以前不能理解為什么并發(fā)要比串行執(zhí)行效率高的原因就是因?yàn)?,我?dāng)時(shí)站的角度并不是多任務(wù)處理,而是單任務(wù)處理的角度,但這種場景一定非常少見,或者可以幾乎說完全不存在,你想一下,你的電腦開機(jī)之后,會(huì)只有一個(gè)任務(wù)再被單獨(dú)處理嗎?絕對(duì)不會(huì),怎么驗(yàn)證呢?非常簡單!你打開你的任務(wù)管理器,去看一下有多少后臺(tái)進(jìn)程正在被運(yùn)行,這會(huì)是單任務(wù)處理的場景嗎?
我當(dāng)時(shí)理解有誤就是絕對(duì),單獨(dú)一個(gè)任務(wù)無論是串行還是并發(fā)執(zhí)行效率都是一樣的,但這個(gè)理解本身并沒有錯(cuò)誤,只不過這樣的場景不存在,我們討論這些線程執(zhí)行效率的前提幾乎都是默認(rèn)在多任務(wù)處理的前提下進(jìn)行討論的!

5.條件變量的基本代碼編寫

1.
這里我們先用全局的互斥鎖和條件變量進(jìn)行簡單的代碼測(cè)試,幫助大家在代碼層面上理解一下條件變量帶來的效果,真正使用條件變量和生產(chǎn)消費(fèi)模型編寫代碼的環(huán)境放在第三部分進(jìn)行講解。
首先我們創(chuàng)建出一批線程,并在線程函數(shù)內(nèi)部對(duì)共享資源tickets進(jìn)行加鎖保護(hù),和使用條件變量來實(shí)現(xiàn)線程之間的同步關(guān)系。在start_routine中,我們讓所有的線程在進(jìn)入臨界區(qū)之后,先去執(zhí)行等待,讓所有的線程都去條件變量里面等著(實(shí)際執(zhí)行pthread_cond_wait時(shí)會(huì)自動(dòng)以原子性的方式釋放當(dāng)前線程持有的鎖),然后由主線程來負(fù)責(zé)喚醒cond中的等待線程,如果是這樣的話,那所有的線程都可以申請(qǐng)到鎖訪問到臨界區(qū),不會(huì)出現(xiàn)饑餓線程。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
2.
當(dāng)主線程調(diào)用pthread_cond_signal喚醒cond隊(duì)列中等待的線程后,可以看到線程搶票的運(yùn)行結(jié)果,非常有順序的執(zhí)行票數(shù)- -,執(zhí)行的順序是12453,并且每個(gè)線程都兼顧到位,沒有出現(xiàn)線程饑餓,無法執(zhí)行票數(shù)- -的情況產(chǎn)生。
主要還是因?yàn)楫?dāng)線程被喚醒,訪問完臨界資源釋放完鎖之后,循環(huán)執(zhí)行代碼,他又會(huì)去執(zhí)行pthread_cond_wait了,此時(shí)就又會(huì)釋放鎖,進(jìn)入等待隊(duì)列,而signal此時(shí)會(huì)繼續(xù)重新喚醒等待隊(duì)列的其他線程。以這樣的方式來讓所有線程都可以申請(qǐng)到鎖。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
這里在補(bǔ)充介紹一個(gè)接口pthread_cond_timedwait,該接口與pthread_cond_wait不同的是,wait接口會(huì)將阻塞等待鎖的線程放入cond的等待隊(duì)列里面,直到有鎖被釋放時(shí),pthread_cond_signal接口會(huì)喚醒cond等待隊(duì)列中的線程。而timedwait是等待鎖一段時(shí)間后,如果鎖未被釋放,那么該接口會(huì)自動(dòng)超時(shí)返回,防止線程長時(shí)間的阻塞等待鎖。但這個(gè)接口并不常用,我們還是重點(diǎn)使用pthread_cond_wait接口。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

3.
當(dāng)調(diào)用pthread_cond_broadcast時(shí),會(huì)喚醒cond阻塞隊(duì)列中的所有等待線程,然后這批線程會(huì)依次按照某種順序競爭鎖,當(dāng)線程使用完鎖訪問完臨界區(qū)之后,就會(huì)釋放鎖,然后重新回到條件變量中進(jìn)行等待,而此時(shí)剩余被喚醒的線程再去競爭鎖,做著上一個(gè)線程同樣的工作。所以打印結(jié)果如下圖所示,喚醒一批線程之后,5個(gè)線程都搶票,每次都是以5個(gè)線程為單位進(jìn)行喚醒。
這就是條件變量帶來的線程同步,讓所有線程先去條件變量中進(jìn)行等待,隨后會(huì)喚醒其中的每一個(gè)線程,喚醒后的線程在訪問完臨界資源后,又會(huì)重新投入等待隊(duì)列當(dāng)中,以這樣的方式來讓所有線程都能夠申請(qǐng)鎖訪問到臨界區(qū)的臨界資源。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

三、基于blockqueue的生產(chǎn)消費(fèi)模型

1.雙阻塞隊(duì)列的多生產(chǎn)多消費(fèi)模型的實(shí)現(xiàn)

1.
上面我們已經(jīng)談?wù)撨^生產(chǎn)消費(fèi)模型的概念和條件變量的代碼實(shí)現(xiàn),現(xiàn)在我們就要用這兩樣工具實(shí)現(xiàn)出基于阻塞隊(duì)列的生產(chǎn)消費(fèi)模型。
原本的計(jì)劃是先將單生成單消費(fèi)一個(gè)阻塞隊(duì)列實(shí)現(xiàn)的生成消費(fèi)模型,但是吧這樣有點(diǎn)簡單了,我們直接上難點(diǎn)的,越難才能越加深大家對(duì)線程同步與互斥,阻塞隊(duì)列,條件變量的使用等等的理解,所以我們直接實(shí)現(xiàn)下面那種生產(chǎn)消費(fèi)模型的代碼,即為多生產(chǎn)多消費(fèi),并且實(shí)現(xiàn)兩個(gè)阻塞隊(duì)列,在這種復(fù)雜環(huán)境下依舊能夠保持線程間的同步與互斥式的訪問共享資源。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
2.
由于要實(shí)現(xiàn)兩個(gè)分別存放不同任務(wù)的阻塞隊(duì)列,那我們直接就寫出來一個(gè)阻塞隊(duì)列的類模板,這樣就可以存放任意類型的對(duì)象,所以下面我們先來完善BlockQueue.hpp文件的代碼,也就是阻塞隊(duì)列的類模板代碼。
我們需要一把鎖來保證阻塞隊(duì)列這個(gè)共享資源訪問的安全性,并且生產(chǎn)線程不滿足生產(chǎn)條件時(shí),比如阻塞隊(duì)列已經(jīng)滿了,則生產(chǎn)線程此時(shí)就不應(yīng)該繼續(xù)生產(chǎn),而是要去cond的隊(duì)列中進(jìn)行wait,直到消費(fèi)線程喚醒生產(chǎn)線程,所以生產(chǎn)線程要有自己的produce cond,簡稱pcond。反過來對(duì)于消費(fèi)者來說同樣如此,所以消費(fèi)者在不滿足消費(fèi)條件的時(shí)候,也要去自己的cond隊(duì)列中進(jìn)行wait,那么消費(fèi)者也應(yīng)該要有自己的consume cond,簡稱ccond。所以類BlockQueue的私有成員應(yīng)該包括_mutex互斥鎖,_ccond,_pcond兩個(gè)條件變量,我們還需要一個(gè)變量來描述阻塞隊(duì)列的容量大小也就是_maxcap,然后再加一個(gè)STL容器queue< T > _q;然后希望定義出來的所有阻塞隊(duì)列的最大容量都是同一個(gè)的,所以_maxcap定義為一個(gè)不可修改的靜態(tài)成員變量,靜態(tài)變量在類內(nèi)只是聲明,類外進(jìn)行初始化,初始化時(shí)需要帶上類名,不用添加static關(guān)鍵字。
阻塞隊(duì)列需要實(shí)現(xiàn)的接口主要為四部分,構(gòu)造函數(shù)內(nèi)需要初始化好互斥鎖以及兩個(gè)條件變量,因?yàn)樽枞?duì)列所使用的鎖和條件變量是局部的(對(duì)象本身就在函數(shù)棧幀中)條件變量和鎖,那么就需要在構(gòu)造函數(shù)內(nèi)進(jìn)行初始化,在析構(gòu)函數(shù)內(nèi)完成銷毀。
除此之外,還需要實(shí)現(xiàn)push和pop兩個(gè)接口,為了保證向隊(duì)列中push元素的安全性,所以接口中要進(jìn)行加鎖和解鎖,然后就是判斷是否滿足push的條件,如果隊(duì)列已經(jīng)滿了,那就不要繼續(xù)push,也就是不要繼續(xù)生產(chǎn)了,而是去pcond的隊(duì)列中進(jìn)行wait,一旦wait執(zhí)行流就會(huì)阻塞停下來,等待被喚醒,如果滿足條件,那直接用STLqueue的push接口push元素即可,非常簡單。push元素之后,我們就該喚醒消費(fèi)線程了,因?yàn)楝F(xiàn)在隊(duì)列中至少有一個(gè)元素,是可以供消費(fèi)者消費(fèi)的,所以直接調(diào)用pthread_cond_signal喚醒ccond的隊(duì)列中的線程即可。最后就是釋放鎖的步驟。
對(duì)于pop來說,由于STLqueue的pop接口不會(huì)返回pop出來的元素,所以我們需要通過輸出型參數(shù)的方式拿到pop出來的元素值。與push的實(shí)現(xiàn)邏輯一樣,pop滿足的條件是隊(duì)列中元素必須不為空,如果為空,則需要去ccond的隊(duì)列中進(jìn)行等待,直到被生產(chǎn)線程喚醒。pop數(shù)據(jù)之后,隊(duì)列中一定至少有一個(gè)空的位置,所以此時(shí)應(yīng)該喚醒生產(chǎn)線程,讓生產(chǎn)線程進(jìn)行元素的push,最后還是不要忘記釋放鎖。
對(duì)于接口的實(shí)現(xiàn),大致邏輯說的差不多了。但在代碼中還有幾個(gè)細(xì)節(jié)需要特別說明一下。我們知道pthread_cond_wait接口是放在臨界區(qū)內(nèi)部的,所以在執(zhí)行wait代碼之前線程是持有鎖的,為了在線程等待期間,其他線程也能申請(qǐng)到鎖并進(jìn)入臨界區(qū),所以在pthread_cond_wait被調(diào)用的時(shí)候,它會(huì)自動(dòng)的以原子性的方式將鎖釋放,并將自己阻塞掛起到pcond的隊(duì)列中。那么當(dāng)隊(duì)列中的某一個(gè)線程被喚醒的時(shí)候,他還是要從pthread_cond_wait開始向后執(zhí)行,所以此時(shí)他還是在臨界區(qū)內(nèi)部,所以在pthread_cond_wait返回的時(shí)候,會(huì)自動(dòng)重新申請(qǐng)鎖,然后繼續(xù)在臨界區(qū)中向后執(zhí)行代碼。另外判斷邏輯的語句必須是while,不能是if,因?yàn)樵诙嗌a(chǎn)多消費(fèi)的情景下,可能出現(xiàn)偽喚醒的情況,比如broadcast喚醒所有生產(chǎn)線程,但實(shí)際空位置只有一個(gè),所以此時(shí)在喚醒之后,某一個(gè)線程競爭到鎖,放入元素之后,隊(duì)列已經(jīng)滿了,然后他釋放了鎖,其他某一個(gè)線程在競爭到鎖之后,如果是if邏輯,那就不會(huì)重新判斷是否滿足,而是直接push元素,那就會(huì)發(fā)生段錯(cuò)誤越界訪問,所以要用while循環(huán)來判斷,保證喚醒的線程一定是在條件滿足的情況下進(jìn)行的push元素。至于喚醒對(duì)方和釋放鎖的順序怎么樣都可以,因?yàn)閱拘褜?duì)方,對(duì)方?jīng)]鎖的話,還是需要阻塞等待鎖被釋放,而如果先釋放鎖的話,由于對(duì)方?jīng)]有被喚醒,那照樣還是拿不到鎖,所以這兩個(gè)接口的調(diào)用順序并不影響接口的功能,所以先寫誰都可以。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

3.
主函數(shù)上層調(diào)用的邏輯就是要?jiǎng)?chuàng)建出多生產(chǎn)多消費(fèi)的線程出來,而且要使用兩個(gè)阻塞隊(duì)列來完成計(jì)算任務(wù)和保存任務(wù)的產(chǎn)生與消費(fèi),所以我們又封裝了一個(gè)BlockQueues類,類中封裝兩個(gè)Blockqueue,一個(gè)存儲(chǔ)計(jì)算任務(wù),一個(gè)存儲(chǔ)保存任務(wù),任務(wù)其實(shí)就是類對(duì)象,所以BlockQueues的類模板參數(shù)分別為C calculate和S save。然后就是創(chuàng)建出阻塞隊(duì)列和多個(gè)生產(chǎn)線程和消費(fèi)線程,以及保存線程。分別對(duì)應(yīng)執(zhí)行的線程函數(shù)是produce,consume,save,然后把BlockQueues類型的指針傳給三個(gè)線程函數(shù),這樣在線程函數(shù)內(nèi)部就可以通過BlockQueues類的兩個(gè)指針成員去調(diào)用阻塞隊(duì)列中的push和pop接口,完成任務(wù)的push和pop。
produce中,我們需要定義出CalTask類的對(duì)象,把這個(gè)任務(wù)對(duì)象push到c_bq(calculate blockqueue)這個(gè)阻塞隊(duì)列中,構(gòu)造對(duì)象需要兩個(gè)操作數(shù),以及操作運(yùn)算符,還需要傳一個(gè)mymath執(zhí)行計(jì)算的函數(shù)指針進(jìn)去,因?yàn)槲覀兿M@些任務(wù)對(duì)象都是可調(diào)用對(duì)象,消費(fèi)者在消費(fèi)的時(shí)候,從隊(duì)列中拿到任務(wù)之后就可以通過調(diào)用()運(yùn)算符重載來完成計(jì)算任務(wù)mymath函數(shù)的調(diào)用,為了在打印的時(shí)候我們看的更加清晰,CalTask類內(nèi)還實(shí)現(xiàn)了toTaskString函數(shù),其實(shí)就是打印出計(jì)算任務(wù)的名稱是什么,比如是1+1=?這樣的名稱,讓我們?cè)诮K端能夠明顯看到是produce線程函數(shù)在被執(zhí)行。由于操作運(yùn)算符有多種,所以定義出了字符串對(duì)象oper包括了5種運(yùn)算符,然后我們又rand生產(chǎn)隨機(jī)數(shù),模擬兩個(gè)操作數(shù)的生成。
consume中,任務(wù)比較艱巨,他需要消費(fèi)計(jì)算任務(wù)CalTask,還需要生產(chǎn)保存任務(wù)SaveTask到s_bq(save blockqueue)保存阻塞隊(duì)列中,消費(fèi)任務(wù)需要傳輸出型參數(shù),也就是一個(gè)空的CalTask對(duì)象t到pop接口中,然后pop結(jié)束后,t對(duì)象即為c_bq中取出的任務(wù)對(duì)象,拿出隊(duì)列中的CalTask對(duì)象后,想要消費(fèi)其實(shí)很簡單,因?yàn)檫@個(gè)對(duì)象實(shí)際是仿函數(shù)對(duì)象,直接通過()調(diào)用即可。然后就是生產(chǎn)保存任務(wù)到阻塞隊(duì)列中,與計(jì)算任務(wù)相同的是,保存任務(wù)對(duì)象也需要實(shí)現(xiàn)為可調(diào)用對(duì)象,這樣在save線程取出任務(wù)對(duì)象時(shí),也可以直接通過()來調(diào)用SaveTask類中的運(yùn)算符重載函數(shù),實(shí)現(xiàn)任務(wù)對(duì)象的消費(fèi)。所以在構(gòu)造SaveTask任務(wù)對(duì)象時(shí),需要傳計(jì)算任務(wù)的名稱也就是一個(gè)string類型的對(duì)象,以便于執(zhí)行保存任務(wù)到文件中時(shí),我們能在文件當(dāng)中看到對(duì)應(yīng)保存的計(jì)算任務(wù)名是什么,然后還需要傳一個(gè)函數(shù)指針Save,該函數(shù)的功能其實(shí)就是進(jìn)行文件操作,將計(jì)算任務(wù)的名稱保存到磁盤文件中。
save中,道理也是相同,要想拿出s_bq中的保存任務(wù)對(duì)象,自然需要通過輸出型參數(shù)來拿出,所以我們傳一個(gè)SaveTask類的空對(duì)象t到s_bq中的pop接口,pop調(diào)用之后,t就是s_bq中取出的保存任務(wù)可調(diào)用對(duì)象,所以消費(fèi)的時(shí)候直接通過()來調(diào)用SaveTask類中的()重載函數(shù)即可完成保存任務(wù),相對(duì)應(yīng)的計(jì)算任務(wù)的名稱就會(huì)保存在磁盤文件中。
三個(gè)線程函數(shù)的具體實(shí)現(xiàn)我們說完了,同樣的在MainCp.cc這個(gè)文件當(dāng)中也有一些細(xì)節(jié)要注意。記得我們?cè)谡務(wù)撊绾伪苊猱a(chǎn)生死鎖問題時(shí),我們說到過一個(gè)寫代碼時(shí)需要注意的點(diǎn)就是,在多線程編程尤其是加鎖的代碼中,盡量將申請(qǐng)的資源統(tǒng)一在開頭處一遍就申請(qǐng)好,不要在代碼中需要的時(shí)候才去申請(qǐng),因?yàn)槟强赡軙?huì)出現(xiàn)一些你根本無法預(yù)料的錯(cuò)誤。害害害,人教人教不會(huì),事教人一教一個(gè)準(zhǔn),沒錯(cuò),我就是那個(gè)不在開頭一遍申請(qǐng)好資源的人,所以我也遇到了我無法解決的bug,確實(shí)令我頭疼了很長時(shí)間。初始化第二個(gè)阻塞隊(duì)列的那行代碼如果放在創(chuàng)建produce和consume線程之后,也就是我注釋掉的那個(gè)地方,你去運(yùn)行吧,保證爽死你,你看到的運(yùn)行結(jié)果就會(huì)是,一會(huì)兒運(yùn)行正常,一會(huì)兒報(bào)段錯(cuò)誤,這對(duì)于剛接觸多線程的萌新來說,友好度直接拉滿。產(chǎn)生那樣現(xiàn)象的原因是因?yàn)?,如果主線程運(yùn)行的足夠快,那就會(huì)出現(xiàn)consume線程還沒將保存任務(wù)放到s_bq之前,主線程的s_bq正好初始化好了,所以程序會(huì)正常運(yùn)行。但如果主線程稍微運(yùn)行的滿了,那就會(huì)出現(xiàn)s_bq還未初始化好,consume線程就已經(jīng)將保存任務(wù)放到s_bq里面了,但s_bq是還沒分配內(nèi)存的野指針,所以此時(shí)就會(huì)報(bào)段錯(cuò)誤,因?yàn)槲覀冊(cè)L問了野指針。所以,老鐵們,盡量在開頭的時(shí)候把需要使用的空間資源就分配好,別等到使用的時(shí)候才去分配,因?yàn)槎嗑€程不好找錯(cuò)誤??!

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

4.
最后一個(gè)文件就是Task.hpp,這個(gè)文件就是我們要實(shí)現(xiàn)的計(jì)算任務(wù)類和保存任務(wù)類,以及計(jì)算的方法和保存的方法。計(jì)算任務(wù)類中要實(shí)現(xiàn)兩個(gè)構(gòu)造函數(shù),一個(gè)是空的構(gòu)造函數(shù),用于main中構(gòu)造出空對(duì)象作為輸出型參數(shù)傳遞給阻塞隊(duì)列的pop接口,另一個(gè)就是構(gòu)造出真正的任務(wù)對(duì)象。類成員只需要兩個(gè)操作數(shù)一個(gè)操作符,外加一個(gè)包裝器即可,因?yàn)榘b器可以包裝很多可調(diào)用對(duì)象,所以如果你想搞一個(gè)仿函數(shù)對(duì)象,或者lambda表達(dá)式或函數(shù)指針來傳給構(gòu)造函數(shù)的話,包裝器類型都是可以接收的,在構(gòu)造函數(shù)內(nèi)部將這些私有成員都初始化好即可。除此之外還需要實(shí)現(xiàn)一個(gè)()運(yùn)算符重載和一個(gè)返回string任務(wù)名的toTaskString函數(shù),為了將可調(diào)用對(duì)象的計(jì)算結(jié)果返回,()運(yùn)算符重載內(nèi)部回調(diào)了mymath的方法,將計(jì)算結(jié)果通過snprintf函數(shù)進(jìn)行字符串格式化到buffer里面,然后用buffer構(gòu)造出string對(duì)象進(jìn)行函數(shù)返回。toTaskString也是將計(jì)算任務(wù)進(jìn)行名稱的格式化到buffer里面,同樣返回一個(gè)由buffer構(gòu)造出的string對(duì)象。
mymath函數(shù)的實(shí)現(xiàn)我就不說了,用switch case語句就可以實(shí)現(xiàn)兩個(gè)操作數(shù)的計(jì)算,這真的可以算是入門級(jí)的代碼實(shí)現(xiàn)了。
SaveTask類成員變量包括保存的計(jì)算任務(wù)名_message,這個(gè)任務(wù)名實(shí)際就是通過CalTask的()運(yùn)算符重載函數(shù)返回的string對(duì)象,傳到我們的SaveTask內(nèi)的構(gòu)造函數(shù)里的,另一個(gè)成員變量就是包裝器,用于包裝將任務(wù)名寫到文件的文件操作方法Save函數(shù)指針。同樣的還需要實(shí)現(xiàn)一個(gè)空的構(gòu)造函數(shù),用于main中調(diào)用pop時(shí),將任務(wù)寫到輸出型參數(shù)空的SaveTask對(duì)象里。為了實(shí)現(xiàn)任務(wù)的消費(fèi),我們也實(shí)現(xiàn)出一個(gè)()運(yùn)算符重載,老樣子,回調(diào)一下包裝器包裝的可調(diào)用對(duì)象即可。
至于Save的實(shí)現(xiàn)也不難,就是比較正常的C語言文件操作,fopen打開文件,fclose關(guān)閉文件,fputs寫入文件。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型
5.
到此為止我們就談完了整個(gè)的雙阻塞隊(duì)列實(shí)現(xiàn)的多生產(chǎn)多消費(fèi)模型,下面是程序的運(yùn)行結(jié)果,我們很好的實(shí)現(xiàn)了計(jì)算任務(wù)的生產(chǎn)消費(fèi),保存任務(wù)的生產(chǎn)消費(fèi),且是在多個(gè)生產(chǎn)者多個(gè)消費(fèi)者的多線程情景下實(shí)現(xiàn)的生產(chǎn)消費(fèi)模型。而能夠?qū)崿F(xiàn)的原因還是因?yàn)槲覀冇墟i來保證多線程訪問共享資源的互斥性,還有條件變量來保證多線程在互斥訪問共享資源時(shí)的同步性。

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型

2.生產(chǎn)消費(fèi)模型高效在哪里?(不影響其他多線程并發(fā)或并行的獲取任務(wù)和執(zhí)行任務(wù))

1.
上面代碼寫完了,我們要來回答一個(gè)非常重要的問題,就是為什么生產(chǎn)消費(fèi)模型是高效的?我并沒有見到他高效在哪里?。≡L問阻塞隊(duì)列這個(gè)共享資源時(shí),不還是得互斥式的訪問么?你憑什么說生產(chǎn)消費(fèi)模型高效呢?
確實(shí)!你說的沒有問題,很正確!但實(shí)際生產(chǎn)消費(fèi)模型根本不是高效在向阻塞隊(duì)列中放元素和從阻塞隊(duì)列中拿元素。而是高效在某一個(gè)線程在向阻塞隊(duì)列中放任務(wù)的時(shí)候,不會(huì)影響其他線程獲取任務(wù),某一個(gè)線程在從阻塞隊(duì)列中拿任務(wù)的時(shí)候,不會(huì)影響其他線程在執(zhí)行任務(wù)。
我們今天所寫的阻塞隊(duì)列中不過是存儲(chǔ)了一些微不足道的計(jì)算任務(wù)或保存任務(wù),執(zhí)行和獲取起來根本不費(fèi)力,但未來線程在真正獲取某些大型任務(wù)比如從數(shù)據(jù)庫,網(wǎng)絡(luò),外設(shè)拿來的用戶數(shù)據(jù)需要處理呢?那在獲取任務(wù)和執(zhí)行任務(wù)的時(shí)候,會(huì)很費(fèi)時(shí)間的。
而生產(chǎn)消費(fèi)模型高效就高效在,你某一個(gè)線程互斥式的從阻塞隊(duì)列中拿任務(wù)或取任務(wù)時(shí),根本就不會(huì)影響我其他多個(gè)線程在獲取任務(wù)或執(zhí)行任務(wù),并且其他多個(gè)線程是在并發(fā)或并行的執(zhí)行任務(wù),效率是很高的!
所以總結(jié)起來就一句話,生產(chǎn)消費(fèi)模型并不高效在放任務(wù)到阻塞隊(duì)列和從阻塞隊(duì)列拿任務(wù),而是真正高效在,某一個(gè)線程拿或放任務(wù)到blockqueue的時(shí)候,并不會(huì)影響其他線程并發(fā)或并行的獲取任務(wù)和執(zhí)行任務(wù)

【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型文章來源地址http://www.zghlxwxcb.cn/news/detail-457201.html

到了這里,關(guān)于【Linux】多線程 --- 線程同步與互斥+生產(chǎn)消費(fèi)模型的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 【Linux】詳解線程第三篇——線程同步和生產(chǎn)消費(fèi)者模型

    【Linux】詳解線程第三篇——線程同步和生產(chǎn)消費(fèi)者模型

    本篇線程同步的內(nèi)容是完全基于線程互斥來講的,如果屏幕前的你對(duì)于線程互斥還不是很了解的話,可以看看我上一篇博客:【Linux】詳解線程第二篇——用黃牛搶陳奕迅演唱會(huì)門票的例子來講解【 線程互斥與鎖 】 上篇線程互斥中重點(diǎn)講了互斥鎖,雖然解決了多線程并發(fā)導(dǎo)

    2024年02月07日
    瀏覽(23)
  • 【linux】線程同步+基于BlockingQueue的生產(chǎn)者消費(fèi)者模型

    【linux】線程同步+基于BlockingQueue的生產(chǎn)者消費(fèi)者模型

    喜歡的點(diǎn)贊,收藏,關(guān)注一下把! 在線程互斥寫了一份搶票的代碼,我們發(fā)現(xiàn)雖然加鎖解決了搶到負(fù)數(shù)票的問題,但是一直都是一個(gè)線程在搶票,它錯(cuò)了嗎,它沒錯(cuò)但是不合理。那我們應(yīng)該如何安全合理的搶票呢? 講個(gè)小故事。 假設(shè)學(xué)校有一個(gè)VIP學(xué)霸自習(xí)室,這個(gè)自習(xí)室有

    2024年02月03日
    瀏覽(24)
  • 【Linux】線程同步 -- 條件變量 | 生產(chǎn)者消費(fèi)者模型 | 自旋鎖 |讀寫鎖

    【Linux】線程同步 -- 條件變量 | 生產(chǎn)者消費(fèi)者模型 | 自旋鎖 |讀寫鎖

    舉一個(gè)例子: 學(xué)生去超市消費(fèi)的時(shí)候,與廠家生產(chǎn)的時(shí)候,兩者互不相沖突。 生產(chǎn)的過程與消費(fèi)的過程 – 解耦 臨時(shí)的保存產(chǎn)品的場所(超時(shí)) – 緩沖區(qū) 模型總結(jié)“321”原則: 3種關(guān)系:生產(chǎn)者和生產(chǎn)者(互斥),消費(fèi)者和消費(fèi)者(互斥),生產(chǎn)者和消費(fèi)者(互斥[保證共享資

    2024年02月14日
    瀏覽(25)
  • 【Linux學(xué)習(xí)】多線程——同步 | 條件變量 | 基于阻塞隊(duì)列的生產(chǎn)者消費(fèi)者模型

    【Linux學(xué)習(xí)】多線程——同步 | 條件變量 | 基于阻塞隊(duì)列的生產(chǎn)者消費(fèi)者模型

    ??作者:一只大喵咪1201 ??專欄:《Linux學(xué)習(xí)》 ??格言: 你只管努力,剩下的交給時(shí)間! 以生活中消費(fèi)者生產(chǎn)者為例: 生活中,我們大部分人都扮演著消費(fèi)者的角色,會(huì)經(jīng)常在超市買東西,比如買方便面,而超市的方便面是由供應(yīng)商生成的。所以我們就是消費(fèi)者,供應(yīng)商

    2024年02月05日
    瀏覽(16)
  • 線程同步--生產(chǎn)者消費(fèi)者模型

    線程同步--生產(chǎn)者消費(fèi)者模型

    條件變量是 線程間共享的全局變量 ,線程間可以通過條件變量進(jìn)行同步控制 條件變量的使用必須依賴于互斥鎖以確保線程安全,線程申請(qǐng)了互斥鎖后,可以調(diào)用特定函數(shù) 進(jìn)入條件變量等待隊(duì)列(同時(shí)釋放互斥鎖) ,其他線程則可以通過條件變量在特定的條件下喚醒該線程( 喚醒后線

    2024年01月19日
    瀏覽(25)
  • 線程同步--生產(chǎn)者消費(fèi)者模型--單例模式線程池

    線程同步--生產(chǎn)者消費(fèi)者模型--單例模式線程池

    條件變量是 線程間共享的全局變量 ,線程間可以通過條件變量進(jìn)行同步控制 條件變量的使用必須依賴于互斥鎖以確保線程安全,線程申請(qǐng)了互斥鎖后,可以調(diào)用特定函數(shù) 進(jìn)入條件變量等待隊(duì)列(同時(shí)釋放互斥鎖) ,其他線程則可以通過條件變量在特定的條件下喚醒該線程( 喚醒后線

    2024年01月20日
    瀏覽(22)
  • 線程同步、生產(chǎn)者消費(fèi)模型和POSIX信號(hào)量

    線程同步、生產(chǎn)者消費(fèi)模型和POSIX信號(hào)量

    gitee倉庫: 1.阻塞隊(duì)列代碼:https://gitee.com/WangZihao64/linux/tree/master/BlockQueue 2.環(huán)形隊(duì)列代碼:https://gitee.com/WangZihao64/linux/tree/master/ringqueue 概念 : 利用線程間共享的全局變量進(jìn)行同步的一種機(jī)制,主要包括兩個(gè)動(dòng)作:一個(gè)線程等待\\\"條件變量的條件成立\\\"而掛起;另一個(gè)線程使“

    2024年02月03日
    瀏覽(23)
  • 【Linux】線程安全-生產(chǎn)者消費(fèi)者模型

    【Linux】線程安全-生產(chǎn)者消費(fèi)者模型

    1個(gè)線程安全的隊(duì)列:只要保證先進(jìn)先出特性的數(shù)據(jù)結(jié)構(gòu)都可以稱為隊(duì)列 這個(gè)隊(duì)列要保證互斥(就是保證當(dāng)前只有一個(gè)線程對(duì)隊(duì)列進(jìn)行操作,其他線程不可以同時(shí)來操作),還要保證同步,當(dāng)生產(chǎn)者將隊(duì)列中填充滿了之后要通知消費(fèi)者來進(jìn)行消費(fèi),消費(fèi)者消費(fèi)之后通知生產(chǎn)者

    2024年02月10日
    瀏覽(25)
  • Linux基于多線程和任務(wù)隊(duì)列實(shí)現(xiàn)生產(chǎn)消費(fèi)模型

    Linux基于多線程和任務(wù)隊(duì)列實(shí)現(xiàn)生產(chǎn)消費(fèi)模型

    目錄 一、生產(chǎn)者消費(fèi)者模型 二、代碼實(shí)現(xiàn)模型 2.1 BlockQueue.hpp 2.2 MainCP.cc 2.3 執(zhí)行結(jié)果 三、效率優(yōu)勢(shì) 將上述圖片邏輯轉(zhuǎn)換成代碼邏輯就是,一批線程充當(dāng)生產(chǎn)者角色,一批線程充當(dāng)消費(fèi)者角色,倉庫是生產(chǎn)者和消費(fèi)者獲取的公共資源!下面我想用 321原則 來解釋這個(gè)模型。 既

    2024年02月09日
    瀏覽(22)
  • 基于互斥鎖的生產(chǎn)者消費(fèi)者模型

    基于互斥鎖的生產(chǎn)者消費(fèi)者模型

    生產(chǎn)者消費(fèi)者模型 是一種常用的 并發(fā)編程模型 ,用于 解決多線程或多進(jìn)程環(huán)境下的協(xié)作問題 。該模型包含兩類角色: 生產(chǎn)者和消費(fèi)者 。 生產(chǎn)者負(fù)責(zé)生成數(shù)據(jù) ,并將數(shù)據(jù)存放到共享的緩沖區(qū)中。 消費(fèi)者則從緩沖區(qū)中獲取數(shù)據(jù) 并進(jìn)行處理。生產(chǎn)者和消費(fèi)者之間通過共享的

    2024年02月12日
    瀏覽(25)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包