一、POSIX信號量
1.阻塞隊(duì)列實(shí)現(xiàn)的生產(chǎn)消費(fèi)模型代碼不足的地方(無法事前得知臨界資源的就緒狀態(tài))
1.在先前我們的生產(chǎn)消費(fèi)模型代碼中,一個(gè)線程如果想要操作臨界資源,也就是對臨界資源做修改的時(shí)候,必須臨界資源是滿足條件的才能修改,否則是無法做出修改的,比如下面的push接口,當(dāng)隊(duì)列滿的時(shí)候,此時(shí)我們稱臨界資源條件不就緒,無法繼續(xù)push,那么線程就應(yīng)該去cond的隊(duì)列中進(jìn)行wait,如果此時(shí)隊(duì)列沒滿,也就是臨界資源條件就緒了,那么就可以繼續(xù)push,調(diào)用_q的push接口。
但是通過代碼你可以看到,如果我們想要判斷臨界資源是否就緒,是不是必須先加鎖然后再判斷?因?yàn)楸旧砼袛嗯R界資源,其實(shí)就是在訪問臨界資源,既然要訪問臨界資源,你需不需要加鎖呢?當(dāng)然是需要的!因?yàn)榕R界資源需要被保護(hù)!
所以我們的代碼就呈現(xiàn)下面這種樣子,由于我們無法事前得知臨界資源的狀態(tài)是否就緒,所以我們必須要先加鎖,然后手動判斷臨界資源的就緒狀態(tài),通過狀態(tài)進(jìn)一步判斷是等待,還是直接對臨界資源進(jìn)行操作。
但如果我們能事前得知,那就不需要加鎖了,因?yàn)槲覀兲崆耙呀?jīng)知道了臨界資源的就緒狀態(tài)了,不再需要手動判斷臨界資源的狀態(tài)。所以如果我們有一把計(jì)數(shù)器,這個(gè)計(jì)數(shù)器來表示臨界資源中小塊兒資源的數(shù)目,比如隊(duì)列中的每個(gè)空間就是小塊兒資源,當(dāng)線程想要對臨界資源做訪問的時(shí)候,先去申請這個(gè)計(jì)數(shù)器,如果這個(gè)計(jì)數(shù)器確實(shí)大于0,那不就說明當(dāng)前隊(duì)列是有空余的位置嗎?那就可以直接向隊(duì)列中push數(shù)據(jù)。如果這個(gè)計(jì)數(shù)器等于0,那就說明當(dāng)前隊(duì)列沒有空余位置了,你不能向隊(duì)列中push數(shù)據(jù)了,而應(yīng)該阻塞等待著,等待計(jì)數(shù)器重新大于0的時(shí)候,你才能繼續(xù)向隊(duì)列中push數(shù)據(jù)。
2.信號量的理解
1.信號量究竟是什么呢?他其實(shí)本質(zhì)是一把計(jì)數(shù)器,一把衡量整體的臨界資源中小塊兒臨界資源數(shù)目多少的計(jì)數(shù)器。所以如果有這把計(jì)數(shù)器的話,我們在重新訪問公共資源之前,就不需要先加鎖,在判斷臨界資源的狀態(tài),再根據(jù)狀態(tài)對臨界資源進(jìn)行操作了。而是直接申請信號量,如果信號量申請成功,那就說明臨界資源條件是就緒的,可以進(jìn)行相應(yīng)的生產(chǎn)消費(fèi)活動。
2.而由于信號量是臨界資源中小塊兒臨界資源的數(shù)目,每個(gè)線程申請到的小塊兒臨界資源是各不相同的,那其實(shí)多個(gè)線程就可以并發(fā)+并行的訪問公共資源的不同區(qū)域。
至于并發(fā)+并行,實(shí)際這兩個(gè)是不沖突的,尤其是公司的服務(wù)器,他一定是并發(fā)+并行運(yùn)行的,你這個(gè)線程在申請到信號量后進(jìn)行操作,并不影響其他線程也申請信號量進(jìn)行操作,當(dāng)然這里說的并發(fā)+并行還是對于生產(chǎn)者和消費(fèi)者之間在對臨界資源進(jìn)行操作時(shí)的關(guān)系,因?yàn)橹挥猩a(chǎn)者和消費(fèi)者之間訪問的才是不同的小塊兒資源。
3.所以在有了信號量之后,我們就能提前得知臨界資源的就緒情況,進(jìn)而能夠決定對臨界資源進(jìn)行什么操作。
每一個(gè)線程想要訪問臨界資源中的小塊兒資源時(shí),都需要先申請信號量,申請信號量成功后,才可以訪問小塊兒資源。那其他線程可不可以申請信號量呢?如果可以的話,信號量是不是共享資源呢?如果想要訪問共享資源,共享資源本身是不是需要被保護(hù)呢?
如果信號量只是簡單的++或- -操作來衡量小塊兒臨界資源的數(shù)目的話,那肯定是不對的,因?yàn)?+和- -的操作不是原子的,信號量的申請和釋放就會有安全問題。所以實(shí)際信號量的申請和釋放并不是簡單的++或- -,他的申請和釋放操作應(yīng)該是原子的,信號量- -實(shí)際對應(yīng)的是P操作,信號量++對應(yīng)的是V操作,所以信號量的核心操作是PV操作,或者叫做PV原語。
3.初步看一下信號量的操作接口
1.信號量的操作接口并不難,PV操作對應(yīng)的就是sem_wait和sem_post接口,作用分別是申請信號量和釋放信號量,而sem_t和以前接觸的pthread_mutex_t等類型一樣,都是pthread庫給我們維護(hù)的一種數(shù)據(jù)類型。
4.環(huán)形隊(duì)列實(shí)現(xiàn)的生產(chǎn)消費(fèi)模型
1.上面我們一直在說信號量的原理以及作用,但信號量的應(yīng)用場景是什么呢?如果用信號量來實(shí)現(xiàn)生產(chǎn)消費(fèi)模型,又該如何實(shí)現(xiàn)呢?
在對臨界資源進(jìn)行操作時(shí),有時(shí)并不需要對整個(gè)臨界資源進(jìn)行操作,而是只需要對某一小塊兒資源進(jìn)行操作,那如果生產(chǎn)線程和消費(fèi)線程都各自對小塊兒資源操作的話,這一小塊兒資源就只有一個(gè)線程在訪問,此時(shí)就不會由于多線程訪問臨界資源而產(chǎn)生安全問題了,那生產(chǎn)線程和消費(fèi)線程就可以并發(fā)或并行的去各自訪問自己的小塊兒臨界資源了,互不干擾,臨界資源不會出現(xiàn)安全問題。
2.像這樣使用小塊兒資源的場景,就適合用環(huán)形隊(duì)列來實(shí)現(xiàn)生產(chǎn)消費(fèi)模型,p向空的位置放數(shù)據(jù),c從有數(shù)據(jù)的空間位置中拿數(shù)據(jù),而且我們保證p和c的操作位置不同,也就是說,p一直向前跑,向每個(gè)空位置放數(shù)據(jù),你c不能超過我p,因?yàn)槟愠^的話沒啥用,前面的位置p還沒有放數(shù)據(jù)呢,你就算拿數(shù)據(jù)拿的也是無效的數(shù)據(jù)。而p也不能套c一個(gè)圈,因?yàn)槟闾琢说脑?,就會出現(xiàn)某一個(gè)位置上的數(shù)據(jù)c還沒拿走呢,你p又過來生產(chǎn)數(shù)據(jù)了,此時(shí)就會發(fā)生數(shù)據(jù)覆蓋的問題。
所以大部分情況下,p和c他們操作的都是不同的位置,如果操作的是不同的位置,p和c就可以并發(fā)+并行的生產(chǎn)和消費(fèi)數(shù)據(jù),本質(zhì)原因就是p和c操作的是不同的小塊資源,互相之間并不影響,而原來的阻塞隊(duì)列是作為整體被使用的,p和c直接用的就是這個(gè)整體資源,你生產(chǎn)的時(shí)候,我就不能消費(fèi),我消費(fèi)的時(shí)候,你就不能生產(chǎn),因?yàn)橐坏┩瑫r(shí)生產(chǎn)和消費(fèi),臨界資源是作為整體被使用,就會出現(xiàn)安全問題,不過今天我們不用擔(dān)心,因?yàn)閜和c操作的是不同的小塊兒資源。
但除大部分情況外,還有小部分情況,比如剛開始環(huán)形隊(duì)列為空的時(shí)候,p和c指向的是同一個(gè)隊(duì)列位置,此時(shí)他們使用的就是同一個(gè)小塊兒資源。或者當(dāng)環(huán)形隊(duì)列為滿的時(shí)候,p和c也會指向同一個(gè)位置,他們使用的也是同一個(gè)小塊資源。那對于這種情況的話,就不能并發(fā)+并行的訪問了,而是只能互斥的訪問。當(dāng)ringqueue為空時(shí),必須p先生產(chǎn),c此時(shí)阻塞。當(dāng)ringqueue為滿時(shí),必須c先消費(fèi),p此時(shí)阻塞。
3.所以想要維護(hù)環(huán)形隊(duì)列的生產(chǎn)消費(fèi)模型,主要的核心工作就是維護(hù)三條規(guī)則,一是消費(fèi)者不能超過生產(chǎn)者,二是消費(fèi)者不能套生產(chǎn)者一個(gè)圈,三是當(dāng)隊(duì)列為空或?yàn)闈M的時(shí)候,我們要保證生產(chǎn)和消費(fèi)的互斥與同步關(guān)系,互斥指的是哪個(gè)線程去單獨(dú)訪問,同步指的是兩個(gè)線程都要去訪問,不能有饑餓問題產(chǎn)生。
我們說過信號量是用來衡量臨界資源中資源數(shù)目的計(jì)數(shù)器,那對于生產(chǎn)者而言,他最看重什么小塊兒資源呢?就是空間資源。對于消費(fèi)者而言,他最看重的是數(shù)據(jù)資源。所以我們可以給空間資源定義一個(gè)信號量,給數(shù)據(jù)資源定義一個(gè)信號量。
5.環(huán)形隊(duì)列的代碼編寫(維持生產(chǎn)之間,消費(fèi)之間,生產(chǎn)消費(fèi)之間的三種關(guān)系)
1.我們寫環(huán)形隊(duì)列的代碼,實(shí)際就是維護(hù)上面所說的三條規(guī)則,維護(hù)前兩條很簡單,因?yàn)橛行盘柫抗苤兀?dāng)信號量為0的時(shí)候,P操作就無法滿足,那就會阻塞。對應(yīng)的其實(shí)就是前兩條規(guī)則,例如,隊(duì)列為空的時(shí)候,spaceSem是大于0的,而dataSem是為0的,那么消費(fèi)者的P操作就無法成功,那他一定是無法消費(fèi)數(shù)據(jù)的,所以此時(shí)c就不會超過p。反過來也一樣,隊(duì)列為滿的情況,大家自己想一下。而維護(hù)生產(chǎn)和消費(fèi)之間的互斥與同步關(guān)系靠的是,剛開始信號量的差異,剛開始設(shè)定信號量的時(shí)候,就把spaceSem設(shè)為隊(duì)列大小,dataSem設(shè)為0,那剛開始的時(shí)候,一定是p先走,生產(chǎn)者的P操作會成功,滿的時(shí)候,自然dataSem就變成了隊(duì)列大小,而spaceSem變?yōu)?,所以此時(shí)一定就是c先走,消費(fèi)者的P操作會成功。這樣設(shè)定初始信號量的不同就可以在隊(duì)列為空和為滿的時(shí)候,保證消費(fèi)者和生產(chǎn)者之間的互斥與同步關(guān)系。
2.原本的講解邏輯其實(shí)是先給大家搞一個(gè)單生產(chǎn)單消費(fèi)的代碼,也就是大部分是生產(chǎn)和消費(fèi)之間的并發(fā)訪問,小部分是生產(chǎn)和消費(fèi)在隊(duì)列為空和為滿時(shí)的互斥與同步。因?yàn)樯厦嫠f的全部話題都是在單生產(chǎn)單消費(fèi)的模型下講述的,所以按照321原則來看,現(xiàn)在只有生產(chǎn)和消費(fèi)的關(guān)系,還缺少生產(chǎn)之間和消費(fèi)之間的關(guān)系。
想著是先搞一個(gè)存儲int數(shù)據(jù)的環(huán)形隊(duì)列,然后搞成單生成單消費(fèi)的模型,進(jìn)階一點(diǎn),我們把int數(shù)據(jù)換為CalTask任務(wù),也就是讓ringqueue存儲任務(wù)對象,但依舊是單生產(chǎn)單消費(fèi)。最后實(shí)現(xiàn)存儲任務(wù)對象的多生產(chǎn)多消費(fèi)模型代碼。
但是上面那樣講解太繁瑣了,畢竟上一篇博客也有了阻塞隊(duì)列的生成消費(fèi)模型的基礎(chǔ),所以下面我們也就不那么啰嗦了,直接上存儲CalTask任務(wù)對象的ringqueue的多生產(chǎn)多消費(fèi)模型代碼。我會將代碼的細(xì)節(jié)講述清楚的。
3.我們將底層的代碼一般稱為設(shè)計(jì)模式,上層調(diào)用的代碼稱為業(yè)務(wù)邏輯,所以設(shè)計(jì)模式一定要和業(yè)務(wù)邏輯進(jìn)行解耦,設(shè)計(jì)模式一定是基于業(yè)務(wù)邏輯產(chǎn)生的。所以下面我們先來談上層調(diào)用的代碼,假設(shè)環(huán)形隊(duì)列已經(jīng)寫好了,談完上層調(diào)用的代碼后,再根據(jù)上層的需求,來回頭實(shí)現(xiàn)底層的RingQueue.hpp的代碼。
在上層中,我們創(chuàng)建出一批生成線程和消費(fèi)線程,讓他們分別執(zhí)行ProductorRoutine和ConsumerRoutine,生產(chǎn)者構(gòu)造和獲取計(jì)算任務(wù),我們通過生成隨機(jī)數(shù)來實(shí)現(xiàn)計(jì)算任務(wù)的構(gòu)造。
而消費(fèi)者拿任務(wù)和執(zhí)行任務(wù),也是通過輸出型參數(shù)的方式來解決。所以其實(shí)你可以看到,無論是環(huán)形隊(duì)列還是阻塞隊(duì)列,上層我們測試的邏輯都是相同的,所以上層這里沒什么好說的,關(guān)鍵在于底層的設(shè)計(jì)模式,底層使用的數(shù)據(jù)結(jié)構(gòu)也就是321原則中交易場所的差別,讓生產(chǎn)消費(fèi)模型的實(shí)現(xiàn)有了差別。
下面是任務(wù)類CalTask的類實(shí)現(xiàn),其實(shí)也沒什么好說的,這個(gè)任務(wù)類在阻塞隊(duì)列的時(shí)候我們就已經(jīng)見到過了,這里也就不過多贅述了。
4.還是來談?wù)勱P(guān)鍵的地方吧。環(huán)形隊(duì)列雖然在邏輯結(jié)構(gòu)上是環(huán)形的,但實(shí)際是通過模運(yùn)算+數(shù)組來實(shí)現(xiàn)的環(huán)形隊(duì)列,所以類成員變量,需要一個(gè)vector。為了方便更改vector的大小,也就是存儲任務(wù)的上限,我們搞一個(gè)_cap也就是容量,來表示vector最大存儲數(shù)據(jù)的個(gè)數(shù)。除此之外就是信號量了,生產(chǎn)者或是消費(fèi)者在生產(chǎn)消費(fèi)之前都需要申請各自的信號量,如果信號量申請成功,才能繼續(xù)向后運(yùn)行,所以信號量的作用其實(shí)也就是掛起等待鎖的作用。所以還需要兩個(gè)信號量來分別給生產(chǎn)者和消費(fèi)者來申請。同時(shí)我們前面也說過,生產(chǎn)者和消費(fèi)者在大部分情況下,訪問的小塊兒資源都是不同的,如何保證訪問的小塊兒資源不同呢?實(shí)際就是通過數(shù)組下標(biāo)來完成的,所以我們定義出兩個(gè)下標(biāo)分別對應(yīng)生產(chǎn)位置和消費(fèi)位置。再通過321原則看一下生產(chǎn)消費(fèi)模型,我們還需要維護(hù)生產(chǎn)之間和消費(fèi)之間的互斥關(guān)系,所以我們需要兩把鎖,保證進(jìn)入環(huán)形隊(duì)列的只能有一個(gè)生產(chǎn)者和一個(gè)消費(fèi)者。這就是基本的類成員變量的設(shè)計(jì)。
可能有人會有疑問,為什么我要搞成兩個(gè)信號量呢?一個(gè)spaceSem信號量表示空間資源,另一個(gè)數(shù)據(jù)資源,我直接用_cap減去spaceSem不就可以了嗎?干嘛要定義兩個(gè)信號量??!你說的確實(shí)沒錯(cuò)!但存在安全隱患,因?yàn)闇p去的操作就不是原子的了,你用_cap減去spaceSem這個(gè)過程是線程不安全的。因?yàn)橹挥行盘柫吭腜V操作才是原子的,才是安全的,如果我們自己徒增許多操作,極大可能是非原子的,所以既然我們都有信號量了,并且人家信號量的操作本身就是原子的,操作起來是線程安全的,那何不多定義幾個(gè)信號量呢?有利無害??!
5.在初始化信號量的時(shí)候,我們剛開始就將spaceSem設(shè)置為環(huán)形隊(duì)列大小,dataSem設(shè)置為0,sem_init的第二個(gè)參數(shù)代表線程間共享,也就是說生產(chǎn)線程之間共享spaceSem信號量,消費(fèi)線程之間共享dataSem信號量。
最為重要的兩個(gè)接口就是Push和Pop,拿Push來說,首先我們進(jìn)行P操作,申請spaceSem信號量,申請成功之后要進(jìn)行加鎖操作,因?yàn)槲覀冃枰WC生產(chǎn)者之間是互斥訪問ringqueue的,然后就是在_productorStep的位置進(jìn)行任務(wù)對象的插入,_productorStep有可能會超過_cap,所以還需要%=_cap,然后就需要釋放鎖,最后V操作的時(shí)候,需要注意的是V操作的是dataSem信號量,因?yàn)槟闵a(chǎn)數(shù)據(jù)之后,數(shù)據(jù)資源不就有了嗎?那dataSem就應(yīng)該變多。Pop的操作正好是和Push的操作反過來的,先申請dataSem信號量,最后釋放spaceSem信號量。
下面這個(gè)問題是當(dāng)初實(shí)現(xiàn)接口時(shí)遇到的問題,圖片中放的代碼已經(jīng)是優(yōu)化好之后的代碼了。
6.下面是單生產(chǎn)單消費(fèi)下的運(yùn)行情況,可以看到如果是單生產(chǎn)單消費(fèi),他的運(yùn)行結(jié)果和條件變量非常非常的相似,當(dāng)生產(chǎn)者在sleep(1)時(shí),打印出來的結(jié)果非常的有順序性,那這是為什么呢?
其實(shí)信號量的實(shí)現(xiàn)原理和條件變量是一樣的,只不過條件變量是通過wait和signal來實(shí)現(xiàn)線程間同步與互斥的,,而信號量是通過wait和post來實(shí)現(xiàn)線程間同步與互斥的,wait和post實(shí)際就是信號量的PV操作,也是PV原語。
所以信號量其實(shí)就是條件變量+手動判斷資源就緒狀態(tài),條件變量解決饑餓問題就是通過喚醒其他線程來實(shí)現(xiàn)的,而信號量解決饑餓問題其實(shí)也是間接通過喚醒其他線程來實(shí)現(xiàn)的,只不過信號量這里不是喚醒,而是釋放其他線程的信號量,也就是V操作其他線程的信號量,一旦V操作了其他線程的信號量,那么只要其他P操作還在阻塞的線程,立馬就不會阻塞了,他們立馬就可以申請信號量成功,然后競爭鎖,進(jìn)入臨界區(qū)。
不過與之前條件變量實(shí)現(xiàn)的阻塞隊(duì)列不同的是,之前的阻塞隊(duì)列用的是一把鎖,所以無論什么時(shí)候都只能串行訪問,而今天的環(huán)形隊(duì)列用的是兩把鎖,生成和消費(fèi)之間是互不影響的,他們沒有理由同時(shí)使用一把鎖,所以他們效率就會高一些,生產(chǎn)和消費(fèi)之間是可以并發(fā)+并行的運(yùn)行的,也就是消費(fèi)在競爭到鎖cmutex,進(jìn)入臨界區(qū)拿走數(shù)據(jù)的同時(shí),生產(chǎn)者也可以競爭到pmutex,進(jìn)入臨界區(qū)生產(chǎn)數(shù)據(jù)。唯一需要互斥的就只有生產(chǎn)之間和消費(fèi)之間需要互斥。
7.下面是多生產(chǎn)多消費(fèi)情況下的打印結(jié)果,當(dāng)然什么也看不出來哈,只能看到一堆消費(fèi)線程和生產(chǎn)線程在瘋狂打印著自己的生產(chǎn)和消費(fèi)提示信息。
但我們心里能夠清楚的意識到,生產(chǎn)之間他們被_pmutex鎖住了,所以生產(chǎn)之間是互斥訪問的,消費(fèi)同樣如此,另外我們通過信號量能夠?qū)崿F(xiàn)單個(gè)生產(chǎn)和單個(gè)消費(fèi)之間的同步與互斥關(guān)系,能夠避免出現(xiàn)數(shù)據(jù)競爭,死鎖等問題。
(其實(shí)我自己當(dāng)時(shí)有一些問題產(chǎn)生,例如當(dāng)生產(chǎn)者之間互相競爭鎖的時(shí)候,不會產(chǎn)生饑餓問題嗎?實(shí)際是有可能出現(xiàn)的,但出現(xiàn)饑餓問題的概率很小,我們可以不考慮這個(gè)饑餓問題,因?yàn)槲覀兯鶎懙拇a并不能完全保證生產(chǎn)者線程之間是公平調(diào)度的,因?yàn)?a target="_blank">操作系統(tǒng)的調(diào)度策略可能導(dǎo)致某些線程獲得更多的執(zhí)行時(shí)間,但這并不是由這段代碼直接導(dǎo)致的。換句話說,我們所寫的代碼不太可能出現(xiàn)生產(chǎn)者線程的饑餓問題。但是如果你對線程調(diào)度的公平性有嚴(yán)格的要求,可以使用條件變量或其他更為高級的同步機(jī)制來實(shí)現(xiàn),條件變量實(shí)際上算是一種很公平的同步機(jī)制了,他能讓所有線程都去排隊(duì)式的來?xiàng)l件變量中進(jìn)行等待,直到其他線程將其喚醒,然后被喚醒的線程會去申請鎖,而不會出現(xiàn)饑餓問題。但在我們上面所寫的代碼中暫時(shí)不用考慮生產(chǎn)線程之間或者是消費(fèi)線程之間的饑餓問題。)
8.最后一個(gè)話題就是老套路了,和當(dāng)時(shí)阻塞隊(duì)列實(shí)現(xiàn)的生產(chǎn)消費(fèi)模型最后提出的問題一樣,我們這里就相當(dāng)于再回顧一下。那既然進(jìn)入環(huán)形隊(duì)列的線程大部分情況下也就只能進(jìn)入一個(gè)生產(chǎn)一個(gè)消費(fèi),那我們創(chuàng)建多生產(chǎn)多消費(fèi)的意義是什么呢?其實(shí)道理還是類似的,放任務(wù)和拿任務(wù)并沒有那么耗時(shí),真正在多任務(wù)處理的情況中,獲取任務(wù)和執(zhí)行任務(wù)才是非常耗時(shí)的!而對于計(jì)算機(jī)來說,多任務(wù)處理的場景又非常的常見,所以很需要多線程之間的協(xié)調(diào)工作。而生產(chǎn)消費(fèi)模型高效在,獲取任務(wù)和執(zhí)行任務(wù)的線程之間在協(xié)調(diào)處理多任務(wù)的時(shí)候,不會出現(xiàn)數(shù)據(jù)競爭,死鎖等安全問題,同時(shí)某個(gè)線程在消費(fèi)或生產(chǎn)任務(wù)的同時(shí),并不會影響其他線程獲取或執(zhí)行任務(wù),所以總體來看,多線程之間還是并發(fā)+并行的獲取和執(zhí)行任務(wù),但為了保證多線程的安全性,我們加了一個(gè)交易場所,保證共享資源的安全,維持多線程的互斥與同步關(guān)系,讓多線程能夠更好的適用于多任務(wù)處理的場景。
二、線程池
1.池化技術(shù)和線程池模型
1.實(shí)際線程池并不難理解,因?yàn)榇蟛糠謺r(shí)間內(nèi),計(jì)算機(jī)都面臨著多任務(wù)處理的難題,而多線程協(xié)調(diào)處理多任務(wù)的場景也就司空見慣了,當(dāng)任務(wù)的數(shù)量比較多,并且要求迅速響應(yīng)任務(wù)處理的情況下,如果現(xiàn)去創(chuàng)建多線程,現(xiàn)去處理任務(wù),那就比較晚了,因?yàn)閯?chuàng)建線程那不就是執(zhí)行pthread庫的代碼嗎?而在linux中,pthread庫的代碼又是封裝了底層的系統(tǒng)調(diào)用,所以還需要將頁表切換為內(nèi)核級頁表,將代碼跳轉(zhuǎn)到內(nèi)核空間執(zhí)行內(nèi)核代碼,處理器級別的切換等等工作,這些不都需要花時(shí)間嗎?如果客戶對性能要求苛刻,要求你迅速響應(yīng)的話,那上面那種現(xiàn)創(chuàng)建線程的方式就有點(diǎn)晚了!所以像線程池這樣的技術(shù)本質(zhì)其實(shí)就是提前創(chuàng)建好一批線程,讓這些線程持續(xù)檢測任務(wù)隊(duì)列中是否有任務(wù),如果有,那就喚醒某個(gè)線程,讓他去拿這個(gè)任務(wù)并且執(zhí)行,如果沒有,那就讓線程掛起等待,我操作系統(tǒng)就一直養(yǎng)著你,等到有任務(wù)的時(shí)候再喚醒你,讓你去執(zhí)行。
那這樣池化的技術(shù)本質(zhì)還是為了應(yīng)對未來的某些需求,能夠提升任務(wù)處理的效率。
實(shí)際生活中也不乏這樣的池化技術(shù),例如疫情期間,大家都屯物資,這是為什么呢?這不也是為了應(yīng)對將來疫情封控嚴(yán)重,大家都出不了門,到時(shí)候沒人賣日常的生活用品了,我們能夠拿出來自己屯的物資嗎?那如果我們不屯物資,等到疫情封控最嚴(yán)重的時(shí)候,再出去買菜買肉什么的,這是不就晚了???或者說你去某些飯店吃飯,你和老板說我要吃西紅柿炒雞蛋,老板說沒問題,你先等一會兒,我去村口的菜園里摘點(diǎn)兒西紅柿,然后再去養(yǎng)雞場蹲母雞,等她下出來蛋后,我拿著西紅柿和雞蛋給你做,那要是等老板做完菜,你是不早就餓過頭了啊!所以老板這樣的方式是不也有些晚了???正確的做法應(yīng)該是老板提前屯一些西紅柿和雞蛋,你點(diǎn)菜的時(shí)候,老板能夠直接拿出來給你做,這些其實(shí)都是我們生活中的池化技術(shù)。
2.而內(nèi)存池也是一種池化技術(shù)的體現(xiàn),當(dāng)我們在調(diào)用malloc或new申請堆空間的時(shí)候,實(shí)際底層會調(diào)用諸如brk,mmap這樣的系統(tǒng)調(diào)用,而執(zhí)行系統(tǒng)調(diào)用是要花時(shí)間的,所以內(nèi)存池會預(yù)先分配一定數(shù)量的內(nèi)存塊并將其存儲在一個(gè)池中,以便程序在需要的時(shí)候能夠快速分配和釋放內(nèi)存,這能夠提高程序的性能和減少內(nèi)存碎片的產(chǎn)生。
3.線程池模型實(shí)際就是生產(chǎn)消費(fèi)模型,我們會在線程池中預(yù)先準(zhǔn)備好并創(chuàng)建出一批線程,然后上層將對應(yīng)的任務(wù)push到任務(wù)隊(duì)列中,休眠的線程如果檢測到任務(wù)隊(duì)列中有任務(wù),那就直接被操作系統(tǒng)喚醒,然后去消費(fèi)并處理任務(wù),喚醒一個(gè)線程的代價(jià)是要比創(chuàng)建一個(gè)線程的代價(jià)小很多的。
而實(shí)際下面線程池的模型不就是我們一直學(xué)的生產(chǎn)消費(fèi)模型嗎?那些任務(wù)線程就是生產(chǎn)者,任務(wù)隊(duì)列就是交易場所,處理線程就是消費(fèi)者。所以聽起來高大上的線程池本質(zhì)還是沒有脫離開我們一直所學(xué)的生產(chǎn)消費(fèi)模型,所以實(shí)現(xiàn)線程池頂多在技巧和細(xì)節(jié)上比以前要求高了一些,但在原理上和生產(chǎn)消費(fèi)模型并無區(qū)別。
2.餓漢與懶漢兩種單例模式
1.在IT行業(yè)里,大佬們和菜雞的兩極分化比較嚴(yán)重,牛逼的是真牛逼,垃圾的是真垃圾,所以大佬們對于一些經(jīng)典的常見的應(yīng)用場景,做出解決方案的總結(jié),這樣針對性的解決方案就是設(shè)計(jì)模式。
而單例模式就是大佬總結(jié)出來的一種經(jīng)典的,常用的,??嫉脑O(shè)計(jì)模式。
單例模式就是只能有一個(gè)實(shí)例化對象的類,這個(gè)類我們可以稱為單例。而實(shí)現(xiàn)單例的方式通常有兩種,分別就是懶漢實(shí)現(xiàn)方式和餓漢實(shí)現(xiàn)方式。
舉一個(gè)形象化的例子,懶漢就是吃完飯,先把碗放下,然后等到下一頓飯的時(shí)候再去洗碗,這就是懶漢方式。而餓漢就是吃完飯,立馬把碗洗了,下一頓吃飯的時(shí)候,就不用再去洗碗了,而是直接拿起碗來吃飯,這就是餓漢實(shí)現(xiàn)方式。
雖然生活中懶漢還是不太好的,因?yàn)樯畋容^亂和邋遢。但在計(jì)算機(jī)中,懶漢方式還是不錯(cuò)的,懶漢最核心的思想就是延遲加載,這樣的方式能夠優(yōu)化服務(wù)器的速度,即為你需要的時(shí)候我再給你分配,你現(xiàn)在還用不著,那我就先不給你分配,這就是延遲加載。
2.像餓漢這樣的方式,實(shí)際是非常常見的,因?yàn)檠訒r(shí)加載這樣的管理思想對于軟硬件資源管理者OS而言,實(shí)際是很優(yōu)的一種管理手段。就比如平常的malloc和new,操作系統(tǒng)底層在開辟空間的時(shí)候,實(shí)際并不是以餓漢的方式來給我們開辟的,而是以懶漢的方式來給我們開辟的。等到程序真正訪問和使用要申請的內(nèi)存空間時(shí),會觸發(fā)缺頁中斷,操作系統(tǒng)此時(shí)知曉之后才會真正給我們在物理內(nèi)存上開辟相應(yīng)申請大小的空間,重新構(gòu)建虛擬和物理的映射關(guān)系,返回對應(yīng)的虛擬地址。
3.實(shí)現(xiàn)餓漢遵循的一個(gè)原則就是加載時(shí)即為開辟時(shí),什么意思呢?就是在類加載的時(shí)候,類的單例對象就已經(jīng)存在于虛擬地址空間中了,并且物理內(nèi)存中也有單例對象所占用的內(nèi)存空間。實(shí)現(xiàn)起來也比較簡單,即在類中提前私有化的創(chuàng)建好一個(gè)靜態(tài)對象,當(dāng)然這個(gè)靜態(tài)對象也是這個(gè)單例類唯一的對象,要實(shí)現(xiàn)對象的唯一還需要私有化構(gòu)造函數(shù),delete掉拷貝構(gòu)造和賦值重載成員函數(shù)。類外使用單例對象時(shí),即通過類名加靜態(tài)方法名的方式得到單例對象的地址,從而訪問其他類成員方法。
實(shí)現(xiàn)懶漢遵循的一個(gè)原則就是需要時(shí)即為開辟時(shí),什么意思呢?就是在類加載的時(shí)候,類的單例對象并不會給你創(chuàng)建,而是當(dāng)你調(diào)用GetInstance()接口的時(shí)候,才會真正分配單例對象的堆空間,這就是典型的懶漢實(shí)現(xiàn)方式。(右邊的懶漢方式實(shí)現(xiàn)單例模式是線程不安全的,解決這種不安全的話題放到實(shí)現(xiàn)懶漢版本的線程池那里,我會詳細(xì)說明線程安全版本的懶漢是如何實(shí)現(xiàn)的。)
3.單例模式的線程池代碼(線程安全的懶漢實(shí)現(xiàn)版本)
1.下面我們實(shí)現(xiàn)的線程池,實(shí)際是一個(gè)自帶任務(wù)隊(duì)列的線程池,其內(nèi)部創(chuàng)建出一大批線程,然后外部可以通過調(diào)用Push接口來向線程池中的任務(wù)隊(duì)列里push任務(wù),線程在沒有任務(wù)的時(shí)候,會一直在自己的條件變量中進(jìn)行等待,當(dāng)上層調(diào)用push接口push任務(wù)時(shí),線程池所實(shí)現(xiàn)的push接口在push任務(wù)之后會調(diào)用signal喚醒條件變量下等待的線程,當(dāng)線程被喚醒之后,就會pop出任務(wù)隊(duì)列中的任務(wù)并執(zhí)行他,這實(shí)際就是消費(fèi)過程。而且由于我們要實(shí)現(xiàn)單例版本的線程池,所以還需要提供getInstance接口來獲取單例對象的地址,外部就可以通過對象指針來調(diào)用ThreadPool類的push接口,進(jìn)行任務(wù)的push。
我們通過vector來管理創(chuàng)建出的線程,通過queue來作為任務(wù)隊(duì)列,由于任務(wù)隊(duì)列是消費(fèi)者和生產(chǎn)者共同訪問的,所以任務(wù)隊(duì)列也需要被保護(hù),所以我們通過互斥鎖mutex來保證任務(wù)隊(duì)列的安全,另外我們再定義出一個(gè)變量num表征線程池中線程的個(gè)數(shù),線程在沒有任務(wù)的時(shí)候需要等待,所以還需要一個(gè)cond,為了實(shí)現(xiàn)線程安全的懶漢單例模式,不僅需要定義出靜態(tài)指針tp,還需要一把互斥鎖singleLock來保證靜態(tài)指針的安全性,因?yàn)榭赡芏鄠€(gè)線程同時(shí)進(jìn)入getInstance創(chuàng)建出多個(gè)對象的實(shí)例。不過這個(gè)互斥鎖我們不再使用pthread原生線程庫的互斥鎖,而是用C++11線程庫的mutex來定義互斥鎖。
2.A. 對于構(gòu)造函數(shù)來說,需要初始化好線程個(gè)數(shù),以及創(chuàng)建出對應(yīng)個(gè)數(shù)的線程,并將每個(gè)線程對象的地址push_back到vector當(dāng)中,除此之外還要初始化好cond和mutex,因?yàn)樗麄兪蔷植康?。需要注意的是,我們用的是之前封裝好的RAII風(fēng)格的線程類來像C++11那樣管理每個(gè)線程對象,所以一旦線程池對象被構(gòu)造,那每個(gè)線程對象也就會被構(gòu)造出來,在構(gòu)造線程對象的同時(shí),線程就會運(yùn)行起來,執(zhí)行對應(yīng)的線程函數(shù)。這就是RAII風(fēng)格的線程創(chuàng)建,當(dāng)對象被創(chuàng)建時(shí)線程跑起來,當(dāng)對象銷毀時(shí)線程就會被銷毀,即為在對象創(chuàng)建時(shí)資源被獲取初始化,在對象銷毀時(shí)資源被釋放回收。
B. 對于析構(gòu)函數(shù)來說,當(dāng)線程池對象被銷毀時(shí),要銷毀destroy cond和mutex,其他成員變量編譯器會調(diào)用他們各自的析構(gòu)函數(shù),我們不用擔(dān)心。
C. 所以緊接著我們就應(yīng)該實(shí)現(xiàn)線程函數(shù),因?yàn)橐坏┚€程池對象被初始化,線程就會跑起來執(zhí)行線程函數(shù),我們的線程函數(shù)實(shí)際就是來執(zhí)行任務(wù)的,所以線程函數(shù)命名為handler_task,實(shí)現(xiàn)handler_task需要解決的第一個(gè)問題其實(shí)就是傳參,如果handler_task是類成員函數(shù),那么他的參數(shù)列表會隱含一個(gè)this指針,所以在調(diào)用RAII風(fēng)格的線程構(gòu)造函數(shù)時(shí),會發(fā)生參數(shù)不匹配的錯(cuò)誤,解決方式也很簡單,只要將handler_task設(shè)置為static成員函數(shù)即可解決傳參的工作。
實(shí)現(xiàn)handler_task第一件事實(shí)際就是加鎖,因?yàn)槲覀冃枰WC訪問任務(wù)隊(duì)列的安全性,所以就需要加鎖,并且為了實(shí)現(xiàn)任務(wù)線程和處理線程之間的同步我們還需要在條件變量中wait,等到被喚醒時(shí)再去拿任務(wù)隊(duì)列中的任務(wù)并執(zhí)行,但是上面所說的一切操作都需要訪問類成員變量,而handler_task是一個(gè)靜態(tài)方法,靜態(tài)成員無法訪問非靜態(tài)成員,線程對象的內(nèi)部還有返回線程名的接口叫做threadname(),線程在執(zhí)行任務(wù)的時(shí)候我還想看到是哪個(gè)線程在執(zhí)行任務(wù),所以在執(zhí)行任務(wù)前我想調(diào)用threadname()接口,想要實(shí)現(xiàn)上面的操作,我們不得不傳一個(gè)結(jié)構(gòu)體threadText到線程函數(shù)里面,結(jié)構(gòu)體中包含線程對象指針和線程池對象指針,通過傳遞包含這兩個(gè)指針的結(jié)構(gòu)體就能完成上面我們所說的一系列操作。我們要保證臨界區(qū)的粒度足夠小,所以執(zhí)行任務(wù),也就是調(diào)用可調(diào)用任務(wù)對象CalTask的()重載函數(shù),就應(yīng)該放在臨界區(qū)外面,因?yàn)榕R界區(qū)是保護(hù)任務(wù)隊(duì)列的,既然任務(wù)已經(jīng)取出來了,那其實(shí)沒必要繼續(xù)加鎖保護(hù),所以t()應(yīng)該放在臨界區(qū)外面。至于加鎖的操作,除我們自己在類內(nèi)封裝一系列接口的使用方式外,還可以直接調(diào)用LockGuard.hpp里面同樣是RAII風(fēng)格的加鎖,即在對象創(chuàng)建時(shí)初始化所,對象銷毀時(shí)自動釋放鎖。
D. 然后就是Push接口,可以看到在Push接口里面,我便使用了RAII風(fēng)格的加鎖,當(dāng)離開代碼塊兒的時(shí)候鎖對象lockGuard會被銷毀,此時(shí)互斥鎖mutex會自動釋放,將任務(wù)push到隊(duì)列之后,便可以喚醒處理線程,線程會從cond的等待隊(duì)列中醒來并重新被調(diào)度去執(zhí)行生產(chǎn)者生產(chǎn)的任務(wù)
E. 最后需要實(shí)現(xiàn)的接口就只剩單例模式了,因?yàn)間etInstance()可能會被多個(gè)線程重入,有可能會構(gòu)建出兩個(gè)對象,這樣就不符合單例模式了,并且在析構(gòu)的時(shí)候還有可能產(chǎn)生內(nèi)存泄露的問題,所以我們要對getInstance()接口進(jìn)行加鎖,保證只有一個(gè)線程能夠進(jìn)入getInstance實(shí)例化出單例對象,當(dāng)某一個(gè)線程實(shí)例化出單例對象之后,之后剩余的所有線程進(jìn)入getInstance時(shí),if條件都不會滿足,但是這樣的效率有點(diǎn)低,因?yàn)楹竺娴木€程如果進(jìn)入getInstance時(shí),還需要先申請鎖,然后才能判斷if條件,那我們就直接雙重判斷空指針,提高判斷的效率,后面的線程不用申請鎖也可以直接拿到單例對象的地址,這樣效率是不是就高起來了呢?
除此之外在聲明單例對象的地址時(shí),我們應(yīng)該用volatile關(guān)鍵字修飾,我們直到volatile關(guān)鍵字是用來保持內(nèi)存可見性的,因?yàn)樵谀承┚幾g器優(yōu)化的場景下,可能會由于只讀取寄存器的值,不讀取內(nèi)存的值而造成判斷失誤,從而產(chǎn)生一系列無法預(yù)知的問題,所以為了避免這樣的問題產(chǎn)生,我們選擇用volatile關(guān)鍵字來修飾單例對象的靜態(tài)指針。(假設(shè)10個(gè)線程都想獲取單例對象的地址,代碼中_tp一直沒有被使用,所以編譯器可能直接將_tp開始為nullptr的值加載到寄存器中,也就是加載到當(dāng)前CPU線程的上下文中,如果之前某個(gè)線程已經(jīng)new過了單例對象,那么當(dāng)前CPU在判斷_tp是否為nullptr的時(shí)候,他不拿物理內(nèi)存的值,而是選擇判斷寄存器的值時(shí),就會發(fā)生第二次實(shí)例化對象,所以我們要用volatile關(guān)鍵字來修飾_tp.)
除此之外還要delete掉成員函數(shù),例如拷貝構(gòu)造和拷貝賦值這兩個(gè)成員函數(shù),避免潛在的第二次實(shí)例化單例對象發(fā)生。
3.下面就是RAII風(fēng)格的封裝線程create,join,destory的小組件Thread.hpp,在調(diào)用pthread_create的時(shí)候,也遇到了this指針傳參不匹配的問題,我們依舊是通過static修飾類成員方法來解決的,當(dāng)然在靜態(tài)方法里還是會遇到相同的問題,那就是沒有this指針無法調(diào)到其他的類成員函數(shù),所以還是老樣子,定義一個(gè)結(jié)構(gòu)體保存this指針和線程函數(shù)的參數(shù),將結(jié)構(gòu)體指針傳遞給線程函數(shù),線程函數(shù)內(nèi)實(shí)際就是解包一下,拿出this指針,回調(diào)包裝器_func包裝的線程池中處理線程執(zhí)行的handler_task方法。
除了構(gòu)造和start_routine有點(diǎn)繞之外,其他函數(shù)都是簡單的對pthread庫中原生接口的封裝,大家簡單看一下就好,這個(gè)RAII風(fēng)格的線程管理小組件實(shí)現(xiàn)起來還是比較簡單的。
4.下面已經(jīng)是老熟人了,我們實(shí)現(xiàn)的阻塞隊(duì)列版本和環(huán)形隊(duì)列版本的生產(chǎn)消費(fèi)模型一直在用這個(gè)任務(wù)組件,這個(gè)任務(wù)組件無非就是構(gòu)造好一個(gè)任務(wù)對象,然后在實(shí)現(xiàn)一個(gè)返回任務(wù)名的函數(shù),以及一個(gè)可調(diào)用對象的()重載,我們不再贅述,大家看一下就行。
5.下面是RAII風(fēng)格加鎖和解鎖的小組件LockGuard.hpp,由外部傳進(jìn)來一把鎖,組件負(fù)責(zé)做加鎖和解鎖的工作,下面實(shí)現(xiàn)的時(shí)候做了多層封裝,其實(shí)沒啥用,只做一層封裝也可以實(shí)現(xiàn)加鎖和解鎖的RAII風(fēng)格的操作。
6.下面就是上層調(diào)用邏輯,獲取單例對象地址,然后通過地址來調(diào)用Push接口去push任務(wù),沒什么好說的,只不過我們實(shí)現(xiàn)了一種用命令行式來構(gòu)建任務(wù)的方式。
三、自旋鎖
1.自旋鎖vs掛起等待鎖
1.除我們之前講的互斥鎖,信號量,條件變量這樣的互斥和同步機(jī)制外,還有很多其他的鎖,例如悲觀鎖,樂觀鎖,但這樣的鎖只是對鎖的作用的一種概念性的統(tǒng)稱,是非?;\統(tǒng)的。另外悲觀鎖的實(shí)現(xiàn)方式:CAS操作和版本號機(jī)制,這些其實(shí)稍微知道一下就行,我們主要使用的還是互斥鎖信號量以及條件變量這樣的方式,有這些其實(shí)目前已經(jīng)夠用了。
但還需要深入知道一些的是自旋鎖和讀寫鎖,這樣的鎖平常我們不怎么用,但屬于我們需要掌握的范疇,了解自旋鎖和讀寫鎖之后,基本上就夠用了。
2.以前我們學(xué)到的互斥鎖,信號量這些,一旦申請失敗,線程就會被阻塞掛起,我們稱這樣的鎖為掛起等待鎖,因?yàn)榫€程需要去PCB維護(hù)的等待隊(duì)列中進(jìn)行wait,直到鎖被釋放。
而自旋鎖如果申請失敗,線程并不會掛起等待,它會選擇自旋,循環(huán)檢查鎖的狀態(tài)是否被釋放,這種方式可以減少線程上下文切換時(shí)所帶來的性能開銷,但同時(shí)也會帶來CPU資源的浪費(fèi),因?yàn)槟氵@個(gè)線程一直霸占CPU不斷輪詢鎖的狀態(tài),CPU無法調(diào)度其他線程了就。
所以使用掛起等待鎖和自旋鎖,主要依據(jù)就是線程需要等待的時(shí)間長短,或者說成是申請到鎖的線程在臨界區(qū)中待的時(shí)間長短,如果時(shí)間較長,那么選擇掛起等待鎖來進(jìn)行加鎖保護(hù)臨界資源的方案就比較合適,如果時(shí)間較短,那么選擇自旋鎖不斷輪詢鎖的狀態(tài),用自旋鎖來進(jìn)行臨界資源的保護(hù)方案就比較合適。
3.緊接著帶來的問題就是,我們該如何衡量時(shí)間的長短呢?又該如何選擇更加合適的加鎖方案呢?
時(shí)間長短其實(shí)沒有答案,因?yàn)闀r(shí)間長短是需要比較才能得出的結(jié)論,而選擇什么樣的加鎖方案,實(shí)際還是要看具體的場景需求。
一般來說臨界區(qū)內(nèi)部如果要進(jìn)行復(fù)雜計(jì)算,IO操作,等待某種軟件條件就緒,大概率我們是要使用掛起等待鎖的。如果只進(jìn)行了特別簡單的操作,例如搶票邏輯,臨界區(qū)的代碼很快就能被執(zhí)行完,那使用自旋鎖就會更加的合適。
但其實(shí)大部分情況下,我們還是用掛起等待鎖,因?yàn)閯e看自旋鎖看起來好像要快一些,一旦時(shí)間評估失誤,那申請自旋鎖的線程就會大量的消耗CPU資源,在多任務(wù)處理的情景下,效率就會降低。除此之外,自旋鎖出現(xiàn)死鎖的時(shí)候,問題要比掛起等待鎖更為嚴(yán)重!如果一個(gè)線程申請互斥鎖時(shí)出現(xiàn)了死鎖,那大不了就是執(zhí)行流阻塞不再運(yùn)行了,但CPU沒啥事?。《孕i出現(xiàn)死鎖時(shí),則會永久性的自旋輪詢鎖的狀態(tài),并且不會從CPU上剝離下去,那么CPU資源就會被一直占用著,無法得到釋放,問題很嚴(yán)重!
當(dāng)然如果你實(shí)在不知道選擇哪種方案的話,可以先默認(rèn)使用掛起等待鎖,然后比較掛起等待鎖和自旋鎖的效率誰高,哪個(gè)高就選擇哪個(gè)方案即可。
4.自旋鎖的操作也并不難,因?yàn)橐驗(yàn)檫@些鎖用的都是POSIX標(biāo)準(zhǔn),所以使用起來很簡單,直接man手冊即可。
2.智能指針和STL容器是否是線程安全的呢?
四、讀寫鎖
1.讀者寫者模型(321原則)
1.除生產(chǎn)消費(fèi)模型之外,還有非常經(jīng)典的一個(gè)模型,就是讀者寫者模型,實(shí)現(xiàn)讀者寫者模型的本質(zhì)其實(shí)也是維護(hù)321原則,即讀者之間,讀者與寫者,寫者之間,以及1個(gè)交易場所,這個(gè)交易場所一般都是數(shù)組,隊(duì)列或者是其他的數(shù)據(jù)結(jié)構(gòu)等等。
讀者就相當(dāng)于消費(fèi)者,寫者就相當(dāng)于生產(chǎn)者,但讀者之間并不是互斥的了,因?yàn)樗c消費(fèi)者最根本的區(qū)別就是讀者不會拿走數(shù)據(jù),也就是不會消費(fèi)數(shù)據(jù),讀者僅僅是對數(shù)據(jù)做讀取,不會進(jìn)行任何修改操作,那么共享資源也就不會因?yàn)槎鄠€(gè)讀者來讀的時(shí)候出現(xiàn)安全問題,都沒人碰你共享資源,你能出啥子問題嘛!所以讀者之間沒有任何的關(guān)系,不想消費(fèi)者之間是互斥關(guān)系,因?yàn)槊總€(gè)消費(fèi)者都要對共享資源做出修改,但我讀者不會這么做,我只讀不改。
而寫者之間肯定是互斥的關(guān)系,因?yàn)槎紝蚕碣Y源寫了!那他們之間不得互斥??!要不然共享資源出了問題咋辦!讀者和寫者之間也是互斥與同步的,當(dāng)讀者讀的時(shí)候,你寫者就不要來寫了,要不然讀者讀到的數(shù)據(jù)都被你寫者給覆蓋掉了!當(dāng)寫者來寫的時(shí)候,你讀者就不要來讀了,你讀到的數(shù)據(jù)都是不完整的,讀個(gè)啥嘛!所以他們之間是互斥的,但當(dāng)讀者寫完的時(shí)候,如果想要數(shù)據(jù)更新,那就應(yīng)該讓寫者來寫了,同樣當(dāng)寫者寫完的時(shí)候,那你讀者就應(yīng)該來讀了。所以讀者和寫者之間是互斥與同步的關(guān)系,既要互斥保證臨界資源的安全,又要同步協(xié)調(diào)完成整個(gè)任務(wù)的處理!
2.那一般什么場景適合用讀者寫者模型呢?例如一次發(fā)布數(shù)據(jù),很長時(shí)間都不會對數(shù)據(jù)做修改,大部分時(shí)間都是被讀取的場景,例如生活中的寫blog,我寫blog是不是大部分時(shí)間都在被別人讀取呢?只有blog出錯(cuò)的時(shí)候,我可能才會重新去修改blog,但大部分時(shí)間blog都是被讀取的。又或是媒體發(fā)布新聞,當(dāng)新聞被發(fā)布的時(shí)候,大部分時(shí)間新聞也都是被讀取的,較小部分時(shí)間才會對發(fā)布的新聞做修改,或者都有可能不做修改。那么對于這樣的場景,使用讀者寫者模型就比較合適了。
3.像實(shí)現(xiàn)生產(chǎn)消費(fèi)模型時(shí),我們一般都會通過cond mutex semaphore這樣的方式實(shí)現(xiàn)blockqueue又或是ringqueue的生產(chǎn)消費(fèi)模型。
那實(shí)現(xiàn)讀者寫者模型時(shí),是不是也應(yīng)該有對應(yīng)的機(jī)制呢?當(dāng)然是有的,pthread庫為我們實(shí)現(xiàn)了讀寫鎖的初始化和銷毀方案,同時(shí)也實(shí)現(xiàn)了分別用于讀者線程間和寫者線程間的加鎖實(shí)現(xiàn),以及讀者寫者統(tǒng)一的解鎖實(shí)現(xiàn)。
—目前所學(xué)的mutex cond sem spin rwlock已經(jīng)能滿足絕大部分需求了
2.讀鎖寫鎖申請的原理(讀鎖共享,寫鎖互斥)
1.下面的表格總結(jié)了讀鎖以及寫鎖被請求時(shí),其他線程的行為。
值得注意的是,多個(gè)讀者之間可以同時(shí)獲取讀鎖,并發(fā)+并行的進(jìn)行讀操作,在設(shè)計(jì)讀寫鎖語義的時(shí)候就是這么設(shè)計(jì)的,它允許多個(gè)讀者之間共享讀寫鎖,并發(fā)+并行的進(jìn)行讀操作。這是讀寫鎖的設(shè)計(jì)語義。
而對于寫鎖來講,那就是典型的互斥鎖語義。
2.讀寫鎖實(shí)現(xiàn)的原理如下,當(dāng)只要出現(xiàn)一個(gè)讀者申請到鎖之后,它會搶寫者的鎖wrlock,所以在多個(gè)讀者進(jìn)行讀取的時(shí)候,reader_count這個(gè)計(jì)數(shù)器就會一直增加,并且在讀者讀取數(shù)據(jù)期間,寫者由于無法申請到wrlock就會一直處于阻塞狀態(tài),寫者無法執(zhí)行寫入的代碼邏輯,會阻塞在自己的lock(&wrlock)代碼處。但讀者之間是可以共享rdlock的,等到所有的讀者都讀完之后,也就是reader_count變?yōu)?的時(shí)候,讀者線程才會釋放wrlock,此時(shí)寫者才能申請到wrlock進(jìn)行寫入。
如果是寫者先申請到鎖的話,讀者能進(jìn)行讀取嗎?當(dāng)然不能!當(dāng)寫者申請到鎖執(zhí)行寫入代碼的時(shí)候,第一個(gè)來的讀者會阻塞在lock(&wrlock)代碼處,因?yàn)榇藭r(shí)wrlock已經(jīng)被寫者拿走了,你讀者想搶的時(shí)候,是搶不到的,你只能阻塞。
但是wrlock和rdlock不一樣,wrlock是不共享的,所以如果有寫著想要申請wrlock的話,和讀者下場一樣,都會阻塞!
這就是我們所說的,讀者讀取的時(shí)候,寫者不能寫入,讀者可以共享讀鎖。寫者寫入的時(shí)候,讀者和其他寫者都無法繼續(xù)執(zhí)行自己的代碼。
3.讀者/寫者優(yōu)先(默認(rèn)讀者優(yōu)先)
1.最后需要談?wù)摰木褪亲x者和寫者優(yōu)先的話題。我們上面所實(shí)現(xiàn)的偽代碼默認(rèn)其實(shí)是讀者優(yōu)先的。
那有人會問,假設(shè)讀者特別多的話,由于第一個(gè)讀者執(zhí)行代碼邏輯的時(shí)候,就已經(jīng)把寫鎖搶走了,那后面無論來多少讀者,我的寫者都無法執(zhí)行寫入,因?yàn)閷懻邿o法申請到rwlock,那就無法進(jìn)入自己代碼的臨界區(qū),那是不是就有可能造成寫者線程的饑餓問題呢?
當(dāng)然是有可能的!但出現(xiàn)寫者線程饑餓的問題是很正常的事情,因?yàn)樽x者寫者模型本身就是大部分時(shí)間在讀取小部分時(shí)間在寫入,那出現(xiàn)寫者線程饑餓本來就很正常嘛!
所以我們默認(rèn)讀者優(yōu)先,如果讀者一直來,那你寫者就一直等!
2.那如果我就想讓寫者優(yōu)先呢?他其實(shí)是可以實(shí)現(xiàn)的,比如10個(gè)讀者要讀,現(xiàn)在已經(jīng)5個(gè)讀者在執(zhí)行讀取的臨界區(qū)代碼了,那寫者線程此時(shí)就阻止后面的5個(gè)線程繼續(xù)執(zhí)行讀者臨界區(qū)的代碼,等到前面5個(gè)讀完,reader_count變?yōu)?了,此時(shí)我寫者要申請rwlock了,我先寫,等我寫完,你們后面5個(gè)讀者再來讀。
原理大概就是上面那樣,但寫者優(yōu)先的策略比較難寫出來,我們就不寫了,知道有讀者寫者優(yōu)先這個(gè)話題就行!
下面是設(shè)置讀寫優(yōu)先的接口pthread_rwlockattr_setkind_np(),np指的是non-portable不可移植的。
-
接口
+關(guān)注
關(guān)注
33文章
8447瀏覽量
150720 -
模型
+關(guān)注
關(guān)注
1文章
3112瀏覽量
48658 -
代碼
+關(guān)注
關(guān)注
30文章
4722瀏覽量
68231 -
Posix
+關(guān)注
關(guān)注
0文章
36瀏覽量
9480
發(fā)布評論請先 登錄
相關(guān)推薦
評論