一区二区三区三上|欧美在线视频五区|国产午夜无码在线观看视频|亚洲国产裸体网站|无码成年人影视|亚洲AV亚洲AV|成人开心激情五月|欧美性爱内射视频|超碰人人干人人上|一区二区无码三区亚洲人区久久精品

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

POSIX信號量的理解

科技綠洲 ? 來源:Linux開發(fā)架構(gòu)之路 ? 作者:Linux開發(fā)架構(gòu)之路 ? 2023-11-09 17:13 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

一、POSIX信號

1.阻塞隊列實現(xiàn)的生產(chǎn)消費模型代碼不足的地方(無法事前得知臨界資源的就緒狀態(tài))

1.在先前我們的生產(chǎn)消費模型代碼中,一個線程如果想要操作臨界資源,也就是對臨界資源做修改的時候,必須臨界資源是滿足條件的才能修改,否則是無法做出修改的,比如下面的push接口,當(dāng)隊列滿的時候,此時我們稱臨界資源條件不就緒,無法繼續(xù)push,那么線程就應(yīng)該去cond的隊列中進(jìn)行wait,如果此時隊列沒滿,也就是臨界資源條件就緒了,那么就可以繼續(xù)push,調(diào)用_q的push接口。

但是通過代碼你可以看到,如果我們想要判斷臨界資源是否就緒,是不是必須先加鎖然后再判斷?因為本身判斷臨界資源,其實就是在訪問臨界資源,既然要訪問臨界資源,你需不需要加鎖呢?當(dāng)然是需要的!因為臨界資源需要被保護(hù)!

所以我們的代碼就呈現(xiàn)下面這種樣子,由于我們無法事前得知臨界資源的狀態(tài)是否就緒,所以我們必須要先加鎖,然后手動判斷臨界資源的就緒狀態(tài),通過狀態(tài)進(jìn)一步判斷是等待,還是直接對臨界資源進(jìn)行操作。

但如果我們能事前得知,那就不需要加鎖了,因為我們提前已經(jīng)知道了臨界資源的就緒狀態(tài)了,不再需要手動判斷臨界資源的狀態(tài)。所以如果我們有一把計數(shù)器,這個計數(shù)器來表示臨界資源中小塊兒資源的數(shù)目,比如隊列中的每個空間就是小塊兒資源,當(dāng)線程想要對臨界資源做訪問的時候,先去申請這個計數(shù)器,如果這個計數(shù)器確實大于0,那不就說明當(dāng)前隊列是有空余的位置嗎?那就可以直接向隊列中push數(shù)據(jù)。如果這個計數(shù)器等于0,那就說明當(dāng)前隊列沒有空余位置了,你不能向隊列中push數(shù)據(jù)了,而應(yīng)該阻塞等待著,等待計數(shù)器重新大于0的時候,你才能繼續(xù)向隊列中push數(shù)據(jù)。

圖片

2.信號量的理解

1.信號量究竟是什么呢?他其實本質(zhì)是一把計數(shù)器,一把衡量整體的臨界資源中小塊兒臨界資源數(shù)目多少的計數(shù)器。所以如果有這把計數(shù)器的話,我們在重新訪問公共資源之前,就不需要先加鎖,在判斷臨界資源的狀態(tài),再根據(jù)狀態(tài)對臨界資源進(jìn)行操作了。而是直接申請信號量,如果信號量申請成功,那就說明臨界資源條件是就緒的,可以進(jìn)行相應(yīng)的生產(chǎn)消費活動。

2.而由于信號量是臨界資源中小塊兒臨界資源的數(shù)目,每個線程申請到的小塊兒臨界資源是各不相同的,那其實多個線程就可以并發(fā)+并行的訪問公共資源的不同區(qū)域。

至于并發(fā)+并行,實際這兩個是不沖突的,尤其是公司的服務(wù)器,他一定是并發(fā)+并行運(yùn)行的,你這個線程在申請到信號量后進(jìn)行操作,并不影響其他線程也申請信號量進(jìn)行操作,當(dāng)然這里說的并發(fā)+并行還是對于生產(chǎn)者和消費者之間在對臨界資源進(jìn)行操作時的關(guān)系,因為只有生產(chǎn)者和消費者之間訪問的才是不同的小塊兒資源。

3.所以在有了信號量之后,我們就能提前得知臨界資源的就緒情況,進(jìn)而能夠決定對臨界資源進(jìn)行什么操作。

每一個線程想要訪問臨界資源中的小塊兒資源時,都需要先申請信號量,申請信號量成功后,才可以訪問小塊兒資源。那其他線程可不可以申請信號量呢?如果可以的話,信號量是不是共享資源呢?如果想要訪問共享資源,共享資源本身是不是需要被保護(hù)呢?

如果信號量只是簡單的++或- -操作來衡量小塊兒臨界資源的數(shù)目的話,那肯定是不對的,因為++和- -的操作不是原子的,信號量的申請和釋放就會有安全問題。所以實際信號量的申請和釋放并不是簡單的++或- -,他的申請和釋放操作應(yīng)該是原子的,信號量- -實際對應(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)形隊列實現(xiàn)的生產(chǎn)消費模型

1.上面我們一直在說信號量的原理以及作用,但信號量的應(yīng)用場景是什么呢?如果用信號量來實現(xiàn)生產(chǎn)消費模型,又該如何實現(xiàn)呢?

在對臨界資源進(jìn)行操作時,有時并不需要對整個臨界資源進(jìn)行操作,而是只需要對某一小塊兒資源進(jìn)行操作,那如果生產(chǎn)線程和消費線程都各自對小塊兒資源操作的話,這一小塊兒資源就只有一個線程在訪問,此時就不會由于多線程訪問臨界資源而產(chǎn)生安全問題了,那生產(chǎn)線程和消費線程就可以并發(fā)或并行的去各自訪問自己的小塊兒臨界資源了,互不干擾,臨界資源不會出現(xiàn)安全問題。

2.像這樣使用小塊兒資源的場景,就適合用環(huán)形隊列來實現(xiàn)生產(chǎn)消費模型,p向空的位置放數(shù)據(jù),c從有數(shù)據(jù)的空間位置中拿數(shù)據(jù),而且我們保證p和c的操作位置不同,也就是說,p一直向前跑,向每個空位置放數(shù)據(jù),你c不能超過我p,因為你超過的話沒啥用,前面的位置p還沒有放數(shù)據(jù)呢,你就算拿數(shù)據(jù)拿的也是無效的數(shù)據(jù)。而p也不能套c一個圈,因為你套了的話,就會出現(xiàn)某一個位置上的數(shù)據(jù)c還沒拿走呢,你p又過來生產(chǎn)數(shù)據(jù)了,此時就會發(fā)生數(shù)據(jù)覆蓋的問題。

所以大部分情況下,p和c他們操作的都是不同的位置,如果操作的是不同的位置,p和c就可以并發(fā)+并行的生產(chǎn)和消費數(shù)據(jù),本質(zhì)原因就是p和c操作的是不同的小塊資源,互相之間并不影響,而原來的阻塞隊列是作為整體被使用的,p和c直接用的就是這個整體資源,你生產(chǎn)的時候,我就不能消費,我消費的時候,你就不能生產(chǎn),因為一旦同時生產(chǎn)和消費,臨界資源是作為整體被使用,就會出現(xiàn)安全問題,不過今天我們不用擔(dān)心,因為p和c操作的是不同的小塊兒資源。

但除大部分情況外,還有小部分情況,比如剛開始環(huán)形隊列為空的時候,p和c指向的是同一個隊列位置,此時他們使用的就是同一個小塊兒資源?;蛘弋?dāng)環(huán)形隊列為滿的時候,p和c也會指向同一個位置,他們使用的也是同一個小塊資源。那對于這種情況的話,就不能并發(fā)+并行的訪問了,而是只能互斥的訪問。當(dāng)ringqueue為空時,必須p先生產(chǎn),c此時阻塞。當(dāng)ringqueue為滿時,必須c先消費,p此時阻塞。

圖片

3.所以想要維護(hù)環(huán)形隊列的生產(chǎn)消費模型,主要的核心工作就是維護(hù)三條規(guī)則,一是消費者不能超過生產(chǎn)者,二是消費者不能套生產(chǎn)者一個圈,三是當(dāng)隊列為空或為滿的時候,我們要保證生產(chǎn)和消費的互斥與同步關(guān)系,互斥指的是哪個線程去單獨訪問,同步指的是兩個線程都要去訪問,不能有饑餓問題產(chǎn)生。

我們說過信號量是用來衡量臨界資源中資源數(shù)目的計數(shù)器,那對于生產(chǎn)者而言,他最看重什么小塊兒資源呢?就是空間資源。對于消費者而言,他最看重的是數(shù)據(jù)資源。所以我們可以給空間資源定義一個信號量,給數(shù)據(jù)資源定義一個信號量。

圖片

5.環(huán)形隊列的代碼編寫(維持生產(chǎn)之間,消費之間,生產(chǎn)消費之間的三種關(guān)系)

1.我們寫環(huán)形隊列的代碼,實際就是維護(hù)上面所說的三條規(guī)則,維護(hù)前兩條很簡單,因為有信號量管著呢,當(dāng)信號量為0的時候,P操作就無法滿足,那就會阻塞。對應(yīng)的其實就是前兩條規(guī)則,例如,隊列為空的時候,spaceSem是大于0的,而dataSem是為0的,那么消費者的P操作就無法成功,那他一定是無法消費數(shù)據(jù)的,所以此時c就不會超過p。反過來也一樣,隊列為滿的情況,大家自己想一下。而維護(hù)生產(chǎn)和消費之間的互斥與同步關(guān)系靠的是,剛開始信號量的差異,剛開始設(shè)定信號量的時候,就把spaceSem設(shè)為隊列大小,dataSem設(shè)為0,那剛開始的時候,一定是p先走,生產(chǎn)者的P操作會成功,滿的時候,自然dataSem就變成了隊列大小,而spaceSem變?yōu)?,所以此時一定就是c先走,消費者的P操作會成功。這樣設(shè)定初始信號量的不同就可以在隊列為空和為滿的時候,保證消費者和生產(chǎn)者之間的互斥與同步關(guān)系。

2.原本的講解邏輯其實是先給大家搞一個單生產(chǎn)單消費的代碼,也就是大部分是生產(chǎn)和消費之間的并發(fā)訪問,小部分是生產(chǎn)和消費在隊列為空和為滿時的互斥與同步。因為上面所說的全部話題都是在單生產(chǎn)單消費的模型下講述的,所以按照321原則來看,現(xiàn)在只有生產(chǎn)和消費的關(guān)系,還缺少生產(chǎn)之間和消費之間的關(guān)系。

想著是先搞一個存儲int數(shù)據(jù)的環(huán)形隊列,然后搞成單生成單消費的模型,進(jìn)階一點,我們把int數(shù)據(jù)換為CalTask任務(wù),也就是讓ringqueue存儲任務(wù)對象,但依舊是單生產(chǎn)單消費。最后實現(xiàn)存儲任務(wù)對象的多生產(chǎn)多消費模型代碼。

但是上面那樣講解太繁瑣了,畢竟上一篇博客也有了阻塞隊列的生成消費模型的基礎(chǔ),所以下面我們也就不那么啰嗦了,直接上存儲CalTask任務(wù)對象的ringqueue的多生產(chǎn)多消費模型代碼。我會將代碼的細(xì)節(jié)講述清楚的。

3.我們將底層的代碼一般稱為設(shè)計模式,上層調(diào)用的代碼稱為業(yè)務(wù)邏輯,所以設(shè)計模式一定要和業(yè)務(wù)邏輯進(jìn)行解耦,設(shè)計模式一定是基于業(yè)務(wù)邏輯產(chǎn)生的。所以下面我們先來談上層調(diào)用的代碼,假設(shè)環(huán)形隊列已經(jīng)寫好了,談完上層調(diào)用的代碼后,再根據(jù)上層的需求,來回頭實現(xiàn)底層的RingQueue.hpp的代碼。

在上層中,我們創(chuàng)建出一批生成線程和消費線程,讓他們分別執(zhí)行ProductorRoutine和ConsumerRoutine,生產(chǎn)者構(gòu)造和獲取計算任務(wù),我們通過生成隨機(jī)數(shù)來實現(xiàn)計算任務(wù)的構(gòu)造。

而消費者拿任務(wù)和執(zhí)行任務(wù),也是通過輸出型參數(shù)的方式來解決。所以其實你可以看到,無論是環(huán)形隊列還是阻塞隊列,上層我們測試的邏輯都是相同的,所以上層這里沒什么好說的,關(guān)鍵在于底層的設(shè)計模式,底層使用的數(shù)據(jù)結(jié)構(gòu)也就是321原則中交易場所的差別,讓生產(chǎn)消費模型的實現(xiàn)有了差別。

圖片

下面是任務(wù)類CalTask的類實現(xiàn),其實也沒什么好說的,這個任務(wù)類在阻塞隊列的時候我們就已經(jīng)見到過了,這里也就不過多贅述了。

圖片

4.還是來談?wù)勱P(guān)鍵的地方吧。環(huán)形隊列雖然在邏輯結(jié)構(gòu)上是環(huán)形的,但實際是通過模運(yùn)算+數(shù)組來實現(xiàn)的環(huán)形隊列,所以類成員變量,需要一個vector。為了方便更改vector的大小,也就是存儲任務(wù)的上限,我們搞一個_cap也就是容量,來表示vector最大存儲數(shù)據(jù)的個數(shù)。除此之外就是信號量了,生產(chǎn)者或是消費者在生產(chǎn)消費之前都需要申請各自的信號量,如果信號量申請成功,才能繼續(xù)向后運(yùn)行,所以信號量的作用其實也就是掛起等待鎖的作用。所以還需要兩個信號量來分別給生產(chǎn)者和消費者來申請。同時我們前面也說過,生產(chǎn)者和消費者在大部分情況下,訪問的小塊兒資源都是不同的,如何保證訪問的小塊兒資源不同呢?實際就是通過數(shù)組下標(biāo)來完成的,所以我們定義出兩個下標(biāo)分別對應(yīng)生產(chǎn)位置和消費位置。再通過321原則看一下生產(chǎn)消費模型,我們還需要維護(hù)生產(chǎn)之間和消費之間的互斥關(guān)系,所以我們需要兩把鎖,保證進(jìn)入環(huán)形隊列的只能有一個生產(chǎn)者和一個消費者。這就是基本的類成員變量的設(shè)計。

可能有人會有疑問,為什么我要搞成兩個信號量呢?一個spaceSem信號量表示空間資源,另一個數(shù)據(jù)資源,我直接用_cap減去spaceSem不就可以了嗎?干嘛要定義兩個信號量??!你說的確實沒錯!但存在安全隱患,因為減去的操作就不是原子的了,你用_cap減去spaceSem這個過程是線程不安全的。因為只有信號量原生的PV操作才是原子的,才是安全的,如果我們自己徒增許多操作,極大可能是非原子的,所以既然我們都有信號量了,并且人家信號量的操作本身就是原子的,操作起來是線程安全的,那何不多定義幾個信號量呢?有利無害??!

5.在初始化信號量的時候,我們剛開始就將spaceSem設(shè)置為環(huán)形隊列大小,dataSem設(shè)置為0,sem_init的第二個參數(shù)代表線程間共享,也就是說生產(chǎn)線程之間共享spaceSem信號量,消費線程之間共享dataSem信號量。

最為重要的兩個接口就是Push和Pop,拿Push來說,首先我們進(jìn)行P操作,申請spaceSem信號量,申請成功之后要進(jìn)行加鎖操作,因為我們需要保證生產(chǎn)者之間是互斥訪問ringqueue的,然后就是在_productorStep的位置進(jìn)行任務(wù)對象的插入,_productorStep有可能會超過_cap,所以還需要%=_cap,然后就需要釋放鎖,最后V操作的時候,需要注意的是V操作的是dataSem信號量,因為你生產(chǎn)數(shù)據(jù)之后,數(shù)據(jù)資源不就有了嗎?那dataSem就應(yīng)該變多。Pop的操作正好是和Push的操作反過來的,先申請dataSem信號量,最后釋放spaceSem信號量。

下面這個問題是當(dāng)初實現(xiàn)接口時遇到的問題,圖片中放的代碼已經(jīng)是優(yōu)化好之后的代碼了。

圖片

圖片

6.下面是單生產(chǎn)單消費下的運(yùn)行情況,可以看到如果是單生產(chǎn)單消費,他的運(yùn)行結(jié)果和條件變量非常非常的相似,當(dāng)生產(chǎn)者在sleep(1)時,打印出來的結(jié)果非常的有順序性,那這是為什么呢?

其實信號量的實現(xiàn)原理和條件變量是一樣的,只不過條件變量是通過wait和signal來實現(xiàn)線程間同步與互斥的,,而信號量是通過wait和post來實現(xiàn)線程間同步與互斥的,wait和post實際就是信號量的PV操作,也是PV原語。

所以信號量其實就是條件變量+手動判斷資源就緒狀態(tài),條件變量解決饑餓問題就是通過喚醒其他線程來實現(xiàn)的,而信號量解決饑餓問題其實也是間接通過喚醒其他線程來實現(xiàn)的,只不過信號量這里不是喚醒,而是釋放其他線程的信號量,也就是V操作其他線程的信號量,一旦V操作了其他線程的信號量,那么只要其他P操作還在阻塞的線程,立馬就不會阻塞了,他們立馬就可以申請信號量成功,然后競爭鎖,進(jìn)入臨界區(qū)。

不過與之前條件變量實現(xiàn)的阻塞隊列不同的是,之前的阻塞隊列用的是一把鎖,所以無論什么時候都只能串行訪問,而今天的環(huán)形隊列用的是兩把鎖,生成和消費之間是互不影響的,他們沒有理由同時使用一把鎖,所以他們效率就會高一些,生產(chǎn)和消費之間是可以并發(fā)+并行的運(yùn)行的,也就是消費在競爭到鎖cmutex,進(jìn)入臨界區(qū)拿走數(shù)據(jù)的同時,生產(chǎn)者也可以競爭到pmutex,進(jìn)入臨界區(qū)生產(chǎn)數(shù)據(jù)。唯一需要互斥的就只有生產(chǎn)之間和消費之間需要互斥。

圖片

7.下面是多生產(chǎn)多消費情況下的打印結(jié)果,當(dāng)然什么也看不出來哈,只能看到一堆消費線程和生產(chǎn)線程在瘋狂打印著自己的生產(chǎn)和消費提示信息。

但我們心里能夠清楚的意識到,生產(chǎn)之間他們被_pmutex鎖住了,所以生產(chǎn)之間是互斥訪問的,消費同樣如此,另外我們通過信號量能夠?qū)崿F(xiàn)單個生產(chǎn)和單個消費之間的同步與互斥關(guān)系,能夠避免出現(xiàn)數(shù)據(jù)競爭,死鎖等問題。

(其實我自己當(dāng)時有一些問題產(chǎn)生,例如當(dāng)生產(chǎn)者之間互相競爭鎖的時候,不會產(chǎn)生饑餓問題嗎?實際是有可能出現(xiàn)的,但出現(xiàn)饑餓問題的概率很小,我們可以不考慮這個饑餓問題,因為我們所寫的代碼并不能完全保證生產(chǎn)者線程之間是公平調(diào)度的,因為操作系統(tǒng)的調(diào)度策略可能導(dǎo)致某些線程獲得更多的執(zhí)行時間,但這并不是由這段代碼直接導(dǎo)致的。換句話說,我們所寫的代碼不太可能出現(xiàn)生產(chǎn)者線程的饑餓問題。但是如果你對線程調(diào)度的公平性有嚴(yán)格的要求,可以使用條件變量或其他更為高級的同步機(jī)制來實現(xiàn),條件變量實際上算是一種很公平的同步機(jī)制了,他能讓所有線程都去排隊式的來條件變量中進(jìn)行等待,直到其他線程將其喚醒,然后被喚醒的線程會去申請鎖,而不會出現(xiàn)饑餓問題。但在我們上面所寫的代碼中暫時不用考慮生產(chǎn)線程之間或者是消費線程之間的饑餓問題。)

圖片

8.最后一個話題就是老套路了,和當(dāng)時阻塞隊列實現(xiàn)的生產(chǎn)消費模型最后提出的問題一樣,我們這里就相當(dāng)于再回顧一下。那既然進(jìn)入環(huán)形隊列的線程大部分情況下也就只能進(jìn)入一個生產(chǎn)一個消費,那我們創(chuàng)建多生產(chǎn)多消費的意義是什么呢?其實道理還是類似的,放任務(wù)和拿任務(wù)并沒有那么耗時,真正在多任務(wù)處理的情況中,獲取任務(wù)和執(zhí)行任務(wù)才是非常耗時的!而對于計算機(jī)來說,多任務(wù)處理的場景又非常的常見,所以很需要多線程之間的協(xié)調(diào)工作。而生產(chǎn)消費模型高效在,獲取任務(wù)和執(zhí)行任務(wù)的線程之間在協(xié)調(diào)處理多任務(wù)的時候,不會出現(xiàn)數(shù)據(jù)競爭,死鎖等安全問題,同時某個線程在消費或生產(chǎn)任務(wù)的同時,并不會影響其他線程獲取或執(zhí)行任務(wù),所以總體來看,多線程之間還是并發(fā)+并行的獲取和執(zhí)行任務(wù),但為了保證多線程的安全性,我們加了一個交易場所,保證共享資源的安全,維持多線程的互斥與同步關(guān)系,讓多線程能夠更好的適用于多任務(wù)處理的場景。

圖片

二、線程池

1.池化技術(shù)和線程池模型

1.實際線程池并不難理解,因為大部分時間內(nèi),計算機(jī)都面臨著多任務(wù)處理的難題,而多線程協(xié)調(diào)處理多任務(wù)的場景也就司空見慣了,當(dāng)任務(wù)的數(shù)量比較多,并且要求迅速響應(yīng)任務(wù)處理的情況下,如果現(xiàn)去創(chuàng)建多線程,現(xiàn)去處理任務(wù),那就比較晚了,因為創(chuàng)建線程那不就是執(zhí)行pthread庫的代碼嗎?而在linux中,pthread庫的代碼又是封裝了底層的系統(tǒng)調(diào)用,所以還需要將頁表切換為內(nèi)核級頁表,將代碼跳轉(zhuǎn)到內(nèi)核空間執(zhí)行內(nèi)核代碼,處理器級別的切換等等工作,這些不都需要花時間嗎?如果客戶對性能要求苛刻,要求你迅速響應(yīng)的話,那上面那種現(xiàn)創(chuàng)建線程的方式就有點晚了!所以像線程池這樣的技術(shù)本質(zhì)其實就是提前創(chuàng)建好一批線程,讓這些線程持續(xù)檢測任務(wù)隊列中是否有任務(wù),如果有,那就喚醒某個線程,讓他去拿這個任務(wù)并且執(zhí)行,如果沒有,那就讓線程掛起等待,我操作系統(tǒng)就一直養(yǎng)著你,等到有任務(wù)的時候再喚醒你,讓你去執(zhí)行。

那這樣池化的技術(shù)本質(zhì)還是為了應(yīng)對未來的某些需求,能夠提升任務(wù)處理的效率。

實際生活中也不乏這樣的池化技術(shù),例如疫情期間,大家都屯物資,這是為什么呢?這不也是為了應(yīng)對將來疫情封控嚴(yán)重,大家都出不了門,到時候沒人賣日常的生活用品了,我們能夠拿出來自己屯的物資嗎?那如果我們不屯物資,等到疫情封控最嚴(yán)重的時候,再出去買菜買肉什么的,這是不就晚了啊?或者說你去某些飯店吃飯,你和老板說我要吃西紅柿炒雞蛋,老板說沒問題,你先等一會兒,我去村口的菜園里摘點兒西紅柿,然后再去養(yǎng)雞場蹲母雞,等她下出來蛋后,我拿著西紅柿和雞蛋給你做,那要是等老板做完菜,你是不早就餓過頭了?。∷岳习暹@樣的方式是不也有些晚了???正確的做法應(yīng)該是老板提前屯一些西紅柿和雞蛋,你點菜的時候,老板能夠直接拿出來給你做,這些其實都是我們生活中的池化技術(shù)。

圖片

2.而內(nèi)存池也是一種池化技術(shù)的體現(xiàn),當(dāng)我們在調(diào)用malloc或new申請堆空間的時候,實際底層會調(diào)用諸如brk,mmap這樣的系統(tǒng)調(diào)用,而執(zhí)行系統(tǒng)調(diào)用是要花時間的,所以內(nèi)存池會預(yù)先分配一定數(shù)量的內(nèi)存塊并將其存儲在一個池中,以便程序在需要的時候能夠快速分配和釋放內(nèi)存,這能夠提高程序的性能和減少內(nèi)存碎片的產(chǎn)生。

圖片

3.線程池模型實際就是生產(chǎn)消費模型,我們會在線程池中預(yù)先準(zhǔn)備好并創(chuàng)建出一批線程,然后上層將對應(yīng)的任務(wù)push到任務(wù)隊列中,休眠的線程如果檢測到任務(wù)隊列中有任務(wù),那就直接被操作系統(tǒng)喚醒,然后去消費并處理任務(wù),喚醒一個線程的代價是要比創(chuàng)建一個線程的代價小很多的。

而實際下面線程池的模型不就是我們一直學(xué)的生產(chǎn)消費模型嗎?那些任務(wù)線程就是生產(chǎn)者,任務(wù)隊列就是交易場所,處理線程就是消費者。所以聽起來高大上的線程池本質(zhì)還是沒有脫離開我們一直所學(xué)的生產(chǎn)消費模型,所以實現(xiàn)線程池頂多在技巧和細(xì)節(jié)上比以前要求高了一些,但在原理上和生產(chǎn)消費模型并無區(qū)別。

圖片

2.餓漢與懶漢兩種單例模式

1.在IT行業(yè)里,大佬們和菜雞的兩極分化比較嚴(yán)重,牛逼的是真牛逼,垃圾的是真垃圾,所以大佬們對于一些經(jīng)典的常見的應(yīng)用場景,做出解決方案的總結(jié),這樣針對性的解決方案就是設(shè)計模式。

而單例模式就是大佬總結(jié)出來的一種經(jīng)典的,常用的,??嫉脑O(shè)計模式。

單例模式就是只能有一個實例化對象的類,這個類我們可以稱為單例。而實現(xiàn)單例的方式通常有兩種,分別就是懶漢實現(xiàn)方式和餓漢實現(xiàn)方式。

舉一個形象化的例子,懶漢就是吃完飯,先把碗放下,然后等到下一頓飯的時候再去洗碗,這就是懶漢方式。而餓漢就是吃完飯,立馬把碗洗了,下一頓吃飯的時候,就不用再去洗碗了,而是直接拿起碗來吃飯,這就是餓漢實現(xiàn)方式。

雖然生活中懶漢還是不太好的,因為生活比較亂和邋遢。但在計算機(jī)中,懶漢方式還是不錯的,懶漢最核心的思想就是延遲加載,這樣的方式能夠優(yōu)化服務(wù)器的速度,即為你需要的時候我再給你分配,你現(xiàn)在還用不著,那我就先不給你分配,這就是延遲加載。

2.像餓漢這樣的方式,實際是非常常見的,因為延時加載這樣的管理思想對于軟硬件資源管理者OS而言,實際是很優(yōu)的一種管理手段。就比如平常的malloc和new,操作系統(tǒng)底層在開辟空間的時候,實際并不是以餓漢的方式來給我們開辟的,而是以懶漢的方式來給我們開辟的。等到程序真正訪問和使用要申請的內(nèi)存空間時,會觸發(fā)缺頁中斷,操作系統(tǒng)此時知曉之后才會真正給我們在物理內(nèi)存上開辟相應(yīng)申請大小的空間,重新構(gòu)建虛擬和物理的映射關(guān)系,返回對應(yīng)的虛擬地址。

圖片

3.實現(xiàn)餓漢遵循的一個原則就是加載時即為開辟時,什么意思呢?就是在類加載的時候,類的單例對象就已經(jīng)存在于虛擬地址空間中了,并且物理內(nèi)存中也有單例對象所占用的內(nèi)存空間。實現(xiàn)起來也比較簡單,即在類中提前私有化的創(chuàng)建好一個靜態(tài)對象,當(dāng)然這個靜態(tài)對象也是這個單例類唯一的對象,要實現(xiàn)對象的唯一還需要私有化構(gòu)造函數(shù),delete掉拷貝構(gòu)造和賦值重載成員函數(shù)。類外使用單例對象時,即通過類名加靜態(tài)方法名的方式得到單例對象的地址,從而訪問其他類成員方法。

實現(xiàn)懶漢遵循的一個原則就是需要時即為開辟時,什么意思呢?就是在類加載的時候,類的單例對象并不會給你創(chuàng)建,而是當(dāng)你調(diào)用GetInstance()接口的時候,才會真正分配單例對象的堆空間,這就是典型的懶漢實現(xiàn)方式。(右邊的懶漢方式實現(xiàn)單例模式是線程不安全的,解決這種不安全的話題放到實現(xiàn)懶漢版本的線程池那里,我會詳細(xì)說明線程安全版本的懶漢是如何實現(xiàn)的。)

圖片

3.單例模式的線程池代碼(線程安全的懶漢實現(xiàn)版本)

1.下面我們實現(xiàn)的線程池,實際是一個自帶任務(wù)隊列的線程池,其內(nèi)部創(chuàng)建出一大批線程,然后外部可以通過調(diào)用Push接口來向線程池中的任務(wù)隊列里push任務(wù),線程在沒有任務(wù)的時候,會一直在自己的條件變量中進(jìn)行等待,當(dāng)上層調(diào)用push接口push任務(wù)時,線程池所實現(xiàn)的push接口在push任務(wù)之后會調(diào)用signal喚醒條件變量下等待的線程,當(dāng)線程被喚醒之后,就會pop出任務(wù)隊列中的任務(wù)并執(zhí)行他,這實際就是消費過程。而且由于我們要實現(xiàn)單例版本的線程池,所以還需要提供getInstance接口來獲取單例對象的地址,外部就可以通過對象指針來調(diào)用ThreadPool類的push接口,進(jìn)行任務(wù)的push。

我們通過vector來管理創(chuàng)建出的線程,通過queue來作為任務(wù)隊列,由于任務(wù)隊列是消費者和生產(chǎn)者共同訪問的,所以任務(wù)隊列也需要被保護(hù),所以我們通過互斥鎖mutex來保證任務(wù)隊列的安全,另外我們再定義出一個變量num表征線程池中線程的個數(shù),線程在沒有任務(wù)的時候需要等待,所以還需要一個cond,為了實現(xiàn)線程安全的懶漢單例模式,不僅需要定義出靜態(tài)指針tp,還需要一把互斥鎖singleLock來保證靜態(tài)指針的安全性,因為可能多個線程同時進(jìn)入getInstance創(chuàng)建出多個對象的實例。不過這個互斥鎖我們不再使用pthread原生線程庫的互斥鎖,而是用C++11線程庫的mutex來定義互斥鎖。

2.A. 對于構(gòu)造函數(shù)來說,需要初始化好線程個數(shù),以及創(chuàng)建出對應(yīng)個數(shù)的線程,并將每個線程對象的地址push_back到vector當(dāng)中,除此之外還要初始化好cond和mutex,因為他們是局部的。需要注意的是,我們用的是之前封裝好的RAII風(fēng)格的線程類來像C++11那樣管理每個線程對象,所以一旦線程池對象被構(gòu)造,那每個線程對象也就會被構(gòu)造出來,在構(gòu)造線程對象的同時,線程就會運(yùn)行起來,執(zhí)行對應(yīng)的線程函數(shù)。這就是RAII風(fēng)格的線程創(chuàng)建,當(dāng)對象被創(chuàng)建時線程跑起來,當(dāng)對象銷毀時線程就會被銷毀,即為在對象創(chuàng)建時資源被獲取初始化,在對象銷毀時資源被釋放回收。

B. 對于析構(gòu)函數(shù)來說,當(dāng)線程池對象被銷毀時,要銷毀destroy cond和mutex,其他成員變量編譯器會調(diào)用他們各自的析構(gòu)函數(shù),我們不用擔(dān)心。

C. 所以緊接著我們就應(yīng)該實現(xiàn)線程函數(shù),因為一旦線程池對象被初始化,線程就會跑起來執(zhí)行線程函數(shù),我們的線程函數(shù)實際就是來執(zhí)行任務(wù)的,所以線程函數(shù)命名為handler_task,實現(xiàn)handler_task需要解決的第一個問題其實就是傳參,如果handler_task是類成員函數(shù),那么他的參數(shù)列表會隱含一個this指針,所以在調(diào)用RAII風(fēng)格的線程構(gòu)造函數(shù)時,會發(fā)生參數(shù)不匹配的錯誤,解決方式也很簡單,只要將handler_task設(shè)置為static成員函數(shù)即可解決傳參的工作。

實現(xiàn)handler_task第一件事實際就是加鎖,因為我們需要保證訪問任務(wù)隊列的安全性,所以就需要加鎖,并且為了實現(xiàn)任務(wù)線程和處理線程之間的同步我們還需要在條件變量中wait,等到被喚醒時再去拿任務(wù)隊列中的任務(wù)并執(zhí)行,但是上面所說的一切操作都需要訪問類成員變量,而handler_task是一個靜態(tài)方法,靜態(tài)成員無法訪問非靜態(tài)成員,線程對象的內(nèi)部還有返回線程名的接口叫做threadname(),線程在執(zhí)行任務(wù)的時候我還想看到是哪個線程在執(zhí)行任務(wù),所以在執(zhí)行任務(wù)前我想調(diào)用threadname()接口,想要實現(xiàn)上面的操作,我們不得不傳一個結(jié)構(gòu)體threadText到線程函數(shù)里面,結(jié)構(gòu)體中包含線程對象指針和線程池對象指針,通過傳遞包含這兩個指針的結(jié)構(gòu)體就能完成上面我們所說的一系列操作。我們要保證臨界區(qū)的粒度足夠小,所以執(zhí)行任務(wù),也就是調(diào)用可調(diào)用任務(wù)對象CalTask的()重載函數(shù),就應(yīng)該放在臨界區(qū)外面,因為臨界區(qū)是保護(hù)任務(wù)隊列的,既然任務(wù)已經(jīng)取出來了,那其實沒必要繼續(xù)加鎖保護(hù),所以t()應(yīng)該放在臨界區(qū)外面。至于加鎖的操作,除我們自己在類內(nèi)封裝一系列接口的使用方式外,還可以直接調(diào)用LockGuard.hpp里面同樣是RAII風(fēng)格的加鎖,即在對象創(chuàng)建時初始化所,對象銷毀時自動釋放鎖。

D. 然后就是Push接口,可以看到在Push接口里面,我便使用了RAII風(fēng)格的加鎖,當(dāng)離開代碼塊兒的時候鎖對象lockGuard會被銷毀,此時互斥鎖mutex會自動釋放,將任務(wù)push到隊列之后,便可以喚醒處理線程,線程會從cond的等待隊列中醒來并重新被調(diào)度去執(zhí)行生產(chǎn)者生產(chǎn)的任務(wù)

E. 最后需要實現(xiàn)的接口就只剩單例模式了,因為getInstance()可能會被多個線程重入,有可能會構(gòu)建出兩個對象,這樣就不符合單例模式了,并且在析構(gòu)的時候還有可能產(chǎn)生內(nèi)存泄露的問題,所以我們要對getInstance()接口進(jìn)行加鎖,保證只有一個線程能夠進(jìn)入getInstance實例化出單例對象,當(dāng)某一個線程實例化出單例對象之后,之后剩余的所有線程進(jìn)入getInstance時,if條件都不會滿足,但是這樣的效率有點低,因為后面的線程如果進(jìn)入getInstance時,還需要先申請鎖,然后才能判斷if條件,那我們就直接雙重判斷空指針,提高判斷的效率,后面的線程不用申請鎖也可以直接拿到單例對象的地址,這樣效率是不是就高起來了呢?

除此之外在聲明單例對象的地址時,我們應(yīng)該用volatile關(guān)鍵字修飾,我們直到volatile關(guān)鍵字是用來保持內(nèi)存可見性的,因為在某些編譯器優(yōu)化的場景下,可能會由于只讀取寄存器的值,不讀取內(nèi)存的值而造成判斷失誤,從而產(chǎn)生一系列無法預(yù)知的問題,所以為了避免這樣的問題產(chǎn)生,我們選擇用volatile關(guān)鍵字來修飾單例對象的靜態(tài)指針。(假設(shè)10個線程都想獲取單例對象的地址,代碼中_tp一直沒有被使用,所以編譯器可能直接將_tp開始為nullptr的值加載到寄存器中,也就是加載到當(dāng)前CPU線程的上下文中,如果之前某個線程已經(jīng)new過了單例對象,那么當(dāng)前CPU在判斷_tp是否為nullptr的時候,他不拿物理內(nèi)存的值,而是選擇判斷寄存器的值時,就會發(fā)生第二次實例化對象,所以我們要用volatile關(guān)鍵字來修飾_tp.)

除此之外還要delete掉成員函數(shù),例如拷貝構(gòu)造和拷貝賦值這兩個成員函數(shù),避免潛在的第二次實例化單例對象發(fā)生。

圖片

3.下面就是RAII風(fēng)格的封裝線程create,join,destory的小組件Thread.hpp,在調(diào)用pthread_create的時候,也遇到了this指針傳參不匹配的問題,我們依舊是通過static修飾類成員方法來解決的,當(dāng)然在靜態(tài)方法里還是會遇到相同的問題,那就是沒有this指針無法調(diào)到其他的類成員函數(shù),所以還是老樣子,定義一個結(jié)構(gòu)體保存this指針和線程函數(shù)的參數(shù),將結(jié)構(gòu)體指針傳遞給線程函數(shù),線程函數(shù)內(nèi)實際就是解包一下,拿出this指針,回調(diào)包裝器_func包裝的線程池中處理線程執(zhí)行的handler_task方法。

除了構(gòu)造和start_routine有點繞之外,其他函數(shù)都是簡單的對pthread庫中原生接口的封裝,大家簡單看一下就好,這個RAII風(fēng)格的線程管理小組件實現(xiàn)起來還是比較簡單的。

圖片

4.下面已經(jīng)是老熟人了,我們實現(xiàn)的阻塞隊列版本和環(huán)形隊列版本的生產(chǎn)消費模型一直在用這個任務(wù)組件,這個任務(wù)組件無非就是構(gòu)造好一個任務(wù)對象,然后在實現(xiàn)一個返回任務(wù)名的函數(shù),以及一個可調(diào)用對象的()重載,我們不再贅述,大家看一下就行。

圖片

5.下面是RAII風(fēng)格加鎖和解鎖的小組件LockGuard.hpp,由外部傳進(jìn)來一把鎖,組件負(fù)責(zé)做加鎖和解鎖的工作,下面實現(xiàn)的時候做了多層封裝,其實沒啥用,只做一層封裝也可以實現(xiàn)加鎖和解鎖的RAII風(fēng)格的操作。

圖片

6.下面就是上層調(diào)用邏輯,獲取單例對象地址,然后通過地址來調(diào)用Push接口去push任務(wù),沒什么好說的,只不過我們實現(xiàn)了一種用命令行式來構(gòu)建任務(wù)的方式。

圖片

三、自旋鎖

1.自旋鎖vs掛起等待鎖

1.除我們之前講的互斥鎖,信號量,條件變量這樣的互斥和同步機(jī)制外,還有很多其他的鎖,例如悲觀鎖,樂觀鎖,但這樣的鎖只是對鎖的作用的一種概念性的統(tǒng)稱,是非?;\統(tǒng)的。另外悲觀鎖的實現(xiàn)方式:CAS操作和版本號機(jī)制,這些其實稍微知道一下就行,我們主要使用的還是互斥鎖信號量以及條件變量這樣的方式,有這些其實目前已經(jīng)夠用了。

但還需要深入知道一些的是自旋鎖和讀寫鎖,這樣的鎖平常我們不怎么用,但屬于我們需要掌握的范疇,了解自旋鎖和讀寫鎖之后,基本上就夠用了。

圖片

2.以前我們學(xué)到的互斥鎖,信號量這些,一旦申請失敗,線程就會被阻塞掛起,我們稱這樣的鎖為掛起等待鎖,因為線程需要去PCB維護(hù)的等待隊列中進(jìn)行wait,直到鎖被釋放。

而自旋鎖如果申請失敗,線程并不會掛起等待,它會選擇自旋,循環(huán)檢查鎖的狀態(tài)是否被釋放,這種方式可以減少線程上下文切換時所帶來的性能開銷,但同時也會帶來CPU資源的浪費,因為你這個線程一直霸占CPU不斷輪詢鎖的狀態(tài),CPU無法調(diào)度其他線程了就。

所以使用掛起等待鎖和自旋鎖,主要依據(jù)就是線程需要等待的時間長短,或者說成是申請到鎖的線程在臨界區(qū)中待的時間長短,如果時間較長,那么選擇掛起等待鎖來進(jìn)行加鎖保護(hù)臨界資源的方案就比較合適,如果時間較短,那么選擇自旋鎖不斷輪詢鎖的狀態(tài),用自旋鎖來進(jìn)行臨界資源的保護(hù)方案就比較合適。

3.緊接著帶來的問題就是,我們該如何衡量時間的長短呢?又該如何選擇更加合適的加鎖方案呢?

時間長短其實沒有答案,因為時間長短是需要比較才能得出的結(jié)論,而選擇什么樣的加鎖方案,實際還是要看具體的場景需求。

一般來說臨界區(qū)內(nèi)部如果要進(jìn)行復(fù)雜計算,IO操作,等待某種軟件條件就緒,大概率我們是要使用掛起等待鎖的。如果只進(jìn)行了特別簡單的操作,例如搶票邏輯,臨界區(qū)的代碼很快就能被執(zhí)行完,那使用自旋鎖就會更加的合適。

但其實大部分情況下,我們還是用掛起等待鎖,因為別看自旋鎖看起來好像要快一些,一旦時間評估失誤,那申請自旋鎖的線程就會大量的消耗CPU資源,在多任務(wù)處理的情景下,效率就會降低。除此之外,自旋鎖出現(xiàn)死鎖的時候,問題要比掛起等待鎖更為嚴(yán)重!如果一個線程申請互斥鎖時出現(xiàn)了死鎖,那大不了就是執(zhí)行流阻塞不再運(yùn)行了,但CPU沒啥事啊!而自旋鎖出現(xiàn)死鎖時,則會永久性的自旋輪詢鎖的狀態(tài),并且不會從CPU上剝離下去,那么CPU資源就會被一直占用著,無法得到釋放,問題很嚴(yán)重!

當(dāng)然如果你實在不知道選擇哪種方案的話,可以先默認(rèn)使用掛起等待鎖,然后比較掛起等待鎖和自旋鎖的效率誰高,哪個高就選擇哪個方案即可。

4.自旋鎖的操作也并不難,因為因為這些鎖用的都是POSIX標(biāo)準(zhǔn),所以使用起來很簡單,直接man手冊即可。

圖片

2.智能指針和STL容器是否是線程安全的呢?

圖片

四、讀寫鎖

1.讀者寫者模型(321原則)

1.除生產(chǎn)消費模型之外,還有非常經(jīng)典的一個模型,就是讀者寫者模型,實現(xiàn)讀者寫者模型的本質(zhì)其實也是維護(hù)321原則,即讀者之間,讀者與寫者,寫者之間,以及1個交易場所,這個交易場所一般都是數(shù)組,隊列或者是其他的數(shù)據(jù)結(jié)構(gòu)等等。

讀者就相當(dāng)于消費者,寫者就相當(dāng)于生產(chǎn)者,但讀者之間并不是互斥的了,因為他與消費者最根本的區(qū)別就是讀者不會拿走數(shù)據(jù),也就是不會消費數(shù)據(jù),讀者僅僅是對數(shù)據(jù)做讀取,不會進(jìn)行任何修改操作,那么共享資源也就不會因為多個讀者來讀的時候出現(xiàn)安全問題,都沒人碰你共享資源,你能出啥子問題嘛!所以讀者之間沒有任何的關(guān)系,不想消費者之間是互斥關(guān)系,因為每個消費者都要對共享資源做出修改,但我讀者不會這么做,我只讀不改。

而寫者之間肯定是互斥的關(guān)系,因為都對共享資源寫了!那他們之間不得互斥??!要不然共享資源出了問題咋辦!讀者和寫者之間也是互斥與同步的,當(dāng)讀者讀的時候,你寫者就不要來寫了,要不然讀者讀到的數(shù)據(jù)都被你寫者給覆蓋掉了!當(dāng)寫者來寫的時候,你讀者就不要來讀了,你讀到的數(shù)據(jù)都是不完整的,讀個啥嘛!所以他們之間是互斥的,但當(dāng)讀者寫完的時候,如果想要數(shù)據(jù)更新,那就應(yīng)該讓寫者來寫了,同樣當(dāng)寫者寫完的時候,那你讀者就應(yīng)該來讀了。所以讀者和寫者之間是互斥與同步的關(guān)系,既要互斥保證臨界資源的安全,又要同步協(xié)調(diào)完成整個任務(wù)的處理!

2.那一般什么場景適合用讀者寫者模型呢?例如一次發(fā)布數(shù)據(jù),很長時間都不會對數(shù)據(jù)做修改,大部分時間都是被讀取的場景,例如生活中的寫blog,我寫blog是不是大部分時間都在被別人讀取呢?只有blog出錯的時候,我可能才會重新去修改blog,但大部分時間blog都是被讀取的。又或是媒體發(fā)布新聞,當(dāng)新聞被發(fā)布的時候,大部分時間新聞也都是被讀取的,較小部分時間才會對發(fā)布的新聞做修改,或者都有可能不做修改。那么對于這樣的場景,使用讀者寫者模型就比較合適了。

3.像實現(xiàn)生產(chǎn)消費模型時,我們一般都會通過cond mutex semaphore這樣的方式實現(xiàn)blockqueue又或是ringqueue的生產(chǎn)消費模型。

那實現(xiàn)讀者寫者模型時,是不是也應(yīng)該有對應(yīng)的機(jī)制呢?當(dāng)然是有的,pthread庫為我們實現(xiàn)了讀寫鎖的初始化和銷毀方案,同時也實現(xiàn)了分別用于讀者線程間和寫者線程間的加鎖實現(xiàn),以及讀者寫者統(tǒng)一的解鎖實現(xiàn)。

圖片

—目前所學(xué)的mutex cond sem spin rwlock已經(jīng)能滿足絕大部分需求了

2.讀鎖寫鎖申請的原理(讀鎖共享,寫鎖互斥)

1.下面的表格總結(jié)了讀鎖以及寫鎖被請求時,其他線程的行為。

值得注意的是,多個讀者之間可以同時獲取讀鎖,并發(fā)+并行的進(jìn)行讀操作,在設(shè)計讀寫鎖語義的時候就是這么設(shè)計的,它允許多個讀者之間共享讀寫鎖,并發(fā)+并行的進(jìn)行讀操作。這是讀寫鎖的設(shè)計語義。

而對于寫鎖來講,那就是典型的互斥鎖語義。

圖片

2.讀寫鎖實現(xiàn)的原理如下,當(dāng)只要出現(xiàn)一個讀者申請到鎖之后,它會搶寫者的鎖wrlock,所以在多個讀者進(jìn)行讀取的時候,reader_count這個計數(shù)器就會一直增加,并且在讀者讀取數(shù)據(jù)期間,寫者由于無法申請到wrlock就會一直處于阻塞狀態(tài),寫者無法執(zhí)行寫入的代碼邏輯,會阻塞在自己的lock(&wrlock)代碼處。但讀者之間是可以共享rdlock的,等到所有的讀者都讀完之后,也就是reader_count變?yōu)?的時候,讀者線程才會釋放wrlock,此時寫者才能申請到wrlock進(jìn)行寫入。

如果是寫者先申請到鎖的話,讀者能進(jìn)行讀取嗎?當(dāng)然不能!當(dāng)寫者申請到鎖執(zhí)行寫入代碼的時候,第一個來的讀者會阻塞在lock(&wrlock)代碼處,因為此時wrlock已經(jīng)被寫者拿走了,你讀者想搶的時候,是搶不到的,你只能阻塞。

但是wrlock和rdlock不一樣,wrlock是不共享的,所以如果有寫著想要申請wrlock的話,和讀者下場一樣,都會阻塞!

這就是我們所說的,讀者讀取的時候,寫者不能寫入,讀者可以共享讀鎖。寫者寫入的時候,讀者和其他寫者都無法繼續(xù)執(zhí)行自己的代碼。

圖片

圖片

3.讀者/寫者優(yōu)先(默認(rèn)讀者優(yōu)先)

1.最后需要談?wù)摰木褪亲x者和寫者優(yōu)先的話題。我們上面所實現(xiàn)的偽代碼默認(rèn)其實是讀者優(yōu)先的。

那有人會問,假設(shè)讀者特別多的話,由于第一個讀者執(zhí)行代碼邏輯的時候,就已經(jīng)把寫鎖搶走了,那后面無論來多少讀者,我的寫者都無法執(zhí)行寫入,因為寫者無法申請到rwlock,那就無法進(jìn)入自己代碼的臨界區(qū),那是不是就有可能造成寫者線程的饑餓問題呢?

當(dāng)然是有可能的!但出現(xiàn)寫者線程饑餓的問題是很正常的事情,因為讀者寫者模型本身就是大部分時間在讀取小部分時間在寫入,那出現(xiàn)寫者線程饑餓本來就很正常嘛!

所以我們默認(rèn)讀者優(yōu)先,如果讀者一直來,那你寫者就一直等!

2.那如果我就想讓寫者優(yōu)先呢?他其實是可以實現(xiàn)的,比如10個讀者要讀,現(xiàn)在已經(jīng)5個讀者在執(zhí)行讀取的臨界區(qū)代碼了,那寫者線程此時就阻止后面的5個線程繼續(xù)執(zhí)行讀者臨界區(qū)的代碼,等到前面5個讀完,reader_count變?yōu)?了,此時我寫者要申請rwlock了,我先寫,等我寫完,你們后面5個讀者再來讀。

原理大概就是上面那樣,但寫者優(yōu)先的策略比較難寫出來,我們就不寫了,知道有讀者寫者優(yōu)先這個話題就行!

下面是設(shè)置讀寫優(yōu)先的接口pthread_rwlockattr_setkind_np(),np指的是non-portable不可移植的。

圖片

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 接口
    +關(guān)注

    關(guān)注

    33

    文章

    9004

    瀏覽量

    153740
  • 模型
    +關(guān)注

    關(guān)注

    1

    文章

    3520

    瀏覽量

    50421
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4900

    瀏覽量

    70734
  • Posix
    +關(guān)注

    關(guān)注

    0

    文章

    36

    瀏覽量

    9864
收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關(guān)推薦
    熱點推薦

    FreeRTOS信號量使用教程

    信號量是操作系統(tǒng)中重要的一部分,信號量一般用來進(jìn)行資源管理和任務(wù)同步, FreeRTOS中信號量又分為二值信號量、 計數(shù)型信號量、互斥
    的頭像 發(fā)表于 12-19 09:22 ?3850次閱讀
    FreeRTOS<b class='flag-5'>信號量</b>使用教程

    UCOS-II:對于信號量,互斥信號量,事件標(biāo)志組的個人理解-轉(zhuǎn)

    。 ucos中提供了好幾個用于同步事件以及共享資源訪問的機(jī)制,目前我看明白的有信號量,互斥信號量,事件標(biāo)志組。下面談?wù)勛约簩λ麄兊?b class='flag-5'>理解:1.互斥信號量:互斥互斥,意思就是我用了你就不能
    發(fā)表于 12-10 21:16

    關(guān)于UCOSIII的信號量和互斥信號量理解

    在UCOSIII中延時一定會引起任務(wù)切換,如果所有任務(wù)都進(jìn)入等待態(tài),則切換到空閑任務(wù)運(yùn)行?請求信號量,如果信號量值非零,不進(jìn)行任務(wù)切換;為零,(等待超時后?或者一般都是設(shè)置死等)進(jìn)行任務(wù)切換?釋放
    發(fā)表于 03-13 00:11

    信號量和互斥信號量理解

    在UCOSIII中,信號量如果要PEND的話,那這個信號量的cnt必須大于等于1才可以(需要在創(chuàng)建的時候設(shè)置第三個參數(shù)cnt為1或者,先POST一下才可以)。這個理解對嗎?互斥信號量,
    發(fā)表于 04-21 02:46

    i.MX6ULL開發(fā)板線程同步POSIX無名信號量

    使用Linux系統(tǒng)提供的機(jī)制來對線程訪問資源的順序進(jìn)行同步,本文檔挑選了信號量,互斥鎖,條件變量來介紹線程同步機(jī)制,實驗代碼在sync/目錄下。1 POSIX無名信號量本章介紹POSIX
    發(fā)表于 04-02 14:04

    信號量和互斥信號量的相關(guān)資料分享

    信號量簡介信號量就是一個上鎖的機(jī)制,代碼必須獲得鑰匙才能執(zhí)行,一旦獲得了信號量,就相當(dāng)于該代碼具有了進(jìn)入被鎖代碼的權(quán)限。說白了,就和java多線程中常用的鎖非常相似。信號量類型在個人的
    發(fā)表于 03-02 07:11

    什么是POSIX無名信號量

    什么是POSIX無名信號量呢?怎樣去使用POSIX無名信號量呢?
    發(fā)表于 03-02 07:38

    信號量機(jī)制怎么理解

    信號量(Semaphore),有時被稱為信號燈,是在多線程環(huán)境下使用的一種設(shè)施,是可以用來保證兩個或多個關(guān)鍵代碼段不被并發(fā)調(diào)用。在進(jìn)入一個關(guān)鍵代碼段之前,線程必須獲取一個信號量;一旦該關(guān)鍵代碼段完成了,那么該線程必須釋放
    發(fā)表于 11-14 09:23 ?2.6w次閱讀
    <b class='flag-5'>信號量</b>機(jī)制怎么<b class='flag-5'>理解</b>

    你了解Linux 各類信號量?

    內(nèi)核信號量與用戶信號量,用戶信號量分為POXIS信號量和SYSTEMV信號量,POXIS信號量
    發(fā)表于 05-04 17:19 ?2669次閱讀
    你了解Linux 各類<b class='flag-5'>信號量</b>?

    Linux IPC POSIX 信號量

    //獲得信號量sem的當(dāng)前的值,放到sval中。如果有線程正在block這個信號量,sval可能返回兩個值,0或“-正在block的線程的數(shù)目”,Linux返回0//成功返回0,失敗返回-1設(shè)
    發(fā)表于 05-16 17:39 ?1037次閱讀

    Linux 多線程信號量同步

    () //如果不再使用信號量,則銷毀信號量 函數(shù)和POSIX IPC的信號量相同例子#include#include#include#include#includechar* b
    發(fā)表于 04-02 14:47 ?502次閱讀

    Linux信號量(2):POSIX 信號量

    上一章,講述了 SYSTEM V 信號量,主要運(yùn)行于進(jìn)程之間,本章主要介紹 POSIX 信號量:有名信號量、無名信號量。
    的頭像 發(fā)表于 10-29 17:34 ?947次閱讀

    FreeRTOS的二值信號量

    FreeRTOS中的信號量是一種任務(wù)間通信的方式,信號量包括:二值信號量、互斥信號量、計數(shù)信號量,本次實驗只使用二值
    的頭像 發(fā)表于 02-10 15:07 ?1788次閱讀

    Free RTOS的計數(shù)型信號量

    上篇講解了二值信號量,二值信號量只能判斷有無,而不能確定事件發(fā)生的次數(shù),因此我們?yōu)榱舜_定事件的次數(shù)引入了計數(shù)型信號量!
    的頭像 發(fā)表于 02-10 15:29 ?1318次閱讀
    Free RTOS的計數(shù)型<b class='flag-5'>信號量</b>

    使用Linux信號量實現(xiàn)互斥點燈

    信號量常用于控制對共享資源的訪問,有計數(shù)型信號量和二值信號量之分。初始化時信號量值大于1的,就是計數(shù)型信號量,計數(shù)型
    的頭像 發(fā)表于 04-13 15:12 ?1066次閱讀
    使用Linux<b class='flag-5'>信號量</b>實現(xiàn)互斥點燈