進(jìn)程PV原語相關(guān)題目的理解

過橋問題

一條河上架設(shè)了由N個(gè)橋墩組成的一座橋。若一個(gè)橋墩只能站一個(gè)人,過河的人只能沿著橋向前走而不能向后退。過河時(shí),只要對岸無人過,就可以過。但不允許河對岸的兩個(gè)人同時(shí)過,以防止出現(xiàn)死鎖。請給出兩個(gè)方向的人順利過河的同步算法。

不妨將兩個(gè)方向稱為正方和反方。為了考慮這個(gè)問題:

  1. 首先要想第一個(gè)過橋人的情況,如果正反雙方都要過橋,那么第一個(gè)上橋的那個(gè)人就獲得對橋的控制權(quán),從而使另一方必須等待。
  2. 其次,假定正方人在過橋,因?yàn)橹挥蠳個(gè)橋墩,每個(gè)橋墩只能站一個(gè)人,所以在正方過橋時(shí)只能控制在N個(gè)人之內(nèi)。
  3. 最后,最后一個(gè)離橋的人要釋放對橋的控制權(quán)。

值得注意的是,第一個(gè)上橋的人(占有橋)不一定是最后一個(gè)離橋的人(釋放橋),這個(gè)問題暗含的一個(gè)思想是,等待過橋的任何一個(gè)人都可能是第一個(gè)上橋的人,正在過橋的這些人中任何一個(gè)都可能是最后一個(gè)離橋的人。

為此,必須解決以下4個(gè)問題:

  • 如何確定是第一個(gè)上橋的人和最后一個(gè)離橋的人?
  • 如何獲得和釋放對橋的控制權(quán)?
  • 誰來獲得、釋放對橋的控制權(quán),為什么?
  • 如何控制在N個(gè)人之內(nèi)?

關(guān)于這四個(gè)問題的解答是:

  • 為了確定第一個(gè)上橋的和最后一個(gè)離橋的人,可以設(shè)置一個(gè)計(jì)數(shù)器counter=0,只要上橋一個(gè)人就令其加一、只要下橋一個(gè)人就令其減一,這樣,如果counter等于1就說明這是第一個(gè)上橋的人,如果counter等于0說明這是最后一個(gè)離橋的人。因?yàn)檫^橋的人都要操作這個(gè)counter,從而counter也就變成了一個(gè)臨界資源,要給它設(shè)置一個(gè)信號量semCounter=1(同時(shí)只能有一個(gè)人來操作變量counter),否則就有可能出現(xiàn)counter的值錯(cuò)亂情況。

  • 因?yàn)檎措p方都要過同一個(gè)橋,所以橋是一個(gè)臨界資源,從而要設(shè)一個(gè)信號量semBridge=1,獲得對橋的控制權(quán)就是要P(semBridge),而釋放對橋的控制權(quán)就是V(semBridge)。

  • 那么誰來獲得、誰來釋放對橋的控制權(quán)呢?也許你會考慮,每一個(gè)上橋的人都執(zhí)行P(semBridge)來獲得上橋資格,每一個(gè)離橋的人都執(zhí)行V(semBridge)來釋放資源。事實(shí)上這是錯(cuò)誤的也是不必要的。這是因?yàn)椋?/p>

    • 每一方過橋的人數(shù)事先是不固定的(盡管不能超過N,但是并不強(qiáng)制規(guī)定一定要是N個(gè)人),所以如果按照這種PV設(shè)計(jì)思路,semBridge的初始值是無法設(shè)置的
    • 如果semBridge初始值設(shè)置為N,就有可能出現(xiàn)2種情況:
      • 正方上橋的人不足N人,他們執(zhí)行P原語后semBridge仍然非負(fù),這樣反方的人執(zhí)行P原語后也上了橋,兩方人員就會相對而發(fā)生死鎖,這是題目不允許的;
      • 又或者正方上橋的人恰好為N個(gè)人,執(zhí)行N次P原語后semBridge為0,這樣反方的人執(zhí)行P原語后進(jìn)入等待狀態(tài),似乎是安全的。但是存在一個(gè)致命的失誤:每一個(gè)離橋的人都要執(zhí)行V原語來釋放。這樣,一旦正方的人至少有一人下橋,反方的人就有可能獲得上橋資格,但時(shí)此時(shí)正方的人尚未全部下橋,因此也會出現(xiàn)死鎖情況。

    正確的策略應(yīng)該是:第一個(gè)上橋的人執(zhí)行P(semBridge),最后一個(gè)離橋的人執(zhí)行V(semBridge)。這樣就能保證一旦正方的人上橋,除非最后一個(gè)人也下橋,否則反方人員必須等待。這也是為什么semBridge的初始值是1的原因。
    ????換句話說,正方向第一個(gè)人執(zhí)行P原語后,第二個(gè)人、第三個(gè)人……都不必執(zhí)行P原語而直接上橋(if語句實(shí)現(xiàn))。
    ????而反方向要上橋時(shí),必然有第一個(gè)上橋人,這個(gè)上橋人必須先對反方的counter的信號量semCounter執(zhí)行P原語,之后因?yàn)闄z測到counter為1后才會對semBridge執(zhí)行P原語,此后進(jìn)入等待,注意因?yàn)檫M(jìn)入等待狀態(tài),信號量semCounter根本沒有被釋放!,從而反方向的第二個(gè)人、第三個(gè)人……依次對semCounter(而不是semBridge)執(zhí)行P原語進(jìn)入等待,根本沒有上橋機(jī)會。

  • 過橋的人數(shù)如何控制在N個(gè)人以內(nèi)呢?設(shè)置一個(gè)控制人數(shù)的信號量semMax=N再適合不過。這個(gè)信號量可以作為正方雙方的共用信號量,因?yàn)閷λ牟僮魇窃讷@取到橋的控制權(quán)(P(semBridge))之后,所以不會發(fā)生沖突。
    注意到在之前已經(jīng)定義了一個(gè)記錄人數(shù)的變量counter,為什么不使用counter來統(tǒng)計(jì)人數(shù)呢?因?yàn)槿绻呀?jīng)有N個(gè)人在過橋,那么正方等待過橋的人就必須等待。信號量的PV操作有令其等待的功能,而單純的變量則沒有,盡管可以對其進(jìn)行數(shù)值比較來查看是否超過人數(shù)上限。

執(zhí)行代碼如下:

//設(shè)置計(jì)數(shù)變量
counterA=0,counterB=0; //正反雙方的計(jì)數(shù)變量
//設(shè)置信號量
semaphore 
    semCounterA=1, semCounterB=1,
    semBridge=1,
    semMax=N
//正方向上橋的過程
void directA(int i)
{
    //判斷是否是第一個(gè)上橋人,若是則鎖住橋
    P(semCounterA);
        counterA++;
        if(counterA==1)
            P(semBridge);
    V(semCounterA);

    //控制人數(shù)不超過N
    P(semMax);
        上橋,過橋,下橋;
    V(semMax);

    //離橋時(shí)減去人數(shù),若是最后一個(gè)離橋則釋放橋
    P(semCounterA);
        counterA--;
        if(counterA==0)
            V(semBridge);
    V(counterA);
}

//反方向與正方向同理
void directB(int i){
    /*....*/
}

注意這個(gè)設(shè)計(jì)的一個(gè)極限情況是,假設(shè)正方向的人開始過橋,并且過橋的人有無窮個(gè)人,那么反方向的人永遠(yuǎn)也無法獲得過橋機(jī)會。因?yàn)椴淮嬖谧詈笠粋€(gè)人來釋放橋。
過橋問題的一個(gè)變種——第二類讀寫者問題將會對此加以限制。

讀寫者問題(第一類)

某數(shù)據(jù)庫有一個(gè)寫進(jìn)程、多個(gè)讀進(jìn)程,它們之間讀、寫操作的互斥要求是:寫進(jìn)程運(yùn)行時(shí),其他讀、寫進(jìn)程不能對數(shù)據(jù)庫進(jìn)行操作。讀進(jìn)程之間不互斥,可以同時(shí)讀數(shù)據(jù)庫。請用信號量及PV操作描述這一組進(jìn)程的工作過程。

分析此題的約束條件:

  1. 讀者之間不發(fā)生互斥
  2. 寫者與讀者(甚至是其他寫者)之間發(fā)生互斥
  3. 數(shù)據(jù)庫是一個(gè)臨界資源區(qū)

多個(gè)讀者之間可以共存,所以第一個(gè)讀者會與寫者競爭資源,一旦讀者進(jìn)入臨界資源區(qū),它就會“鎖”住這個(gè)資源,寫者進(jìn)入等待,而其他讀者可以進(jìn)入。最后一個(gè)讀者退出臨界資源區(qū)時(shí)釋放資源。
為了識別第一個(gè)讀者和最后一個(gè)讀者,設(shè)置計(jì)數(shù)變量readerCounter=0,計(jì)數(shù)變量保護(hù)信號量semReaderCounter=1。數(shù)據(jù)庫資源區(qū)公有信號量semDB=1
由于讀者的個(gè)數(shù)不受限制,所以沒有必要設(shè)置semMax
由此寫出的偽代碼是:

//讀者技術(shù)變量
readerCounter=0;

//信號量
semaphore
    semReaderCounter=1,
    semDB=1;

//寫者進(jìn)程
writer(){
    P(semDB)
        寫操作
    V(semDB)
}

//讀者進(jìn)程
reader(){
    //判定是否是第一個(gè)讀者,如果是則競爭
    P(semReaderCounter)
        semReaderCounter++;
        if(semReaderCounter==1)
            P(semDB)
    V(semReaderCounter)
    
    執(zhí)行讀操作
    
    //判定是否是最后一個(gè)離開數(shù)據(jù)庫的讀者,若是,則釋放資源
    P(semReaderCounter)
        semReaderCounter--;
        if(semReaderCounter==0)
            V(semDB)
    V(semReaderCounter)
}

注意到,此時(shí)也存在一個(gè)隱患:一旦讀者進(jìn)程爭取到數(shù)據(jù)庫,并且此后有源源不斷的讀者進(jìn)程也進(jìn)入數(shù)據(jù)庫,且讀取時(shí)間很長,數(shù)據(jù)庫資源得不到釋放,那么寫者進(jìn)程就有可能長時(shí)間被阻塞而發(fā)生“饑餓現(xiàn)象”。為此,考慮第二類的讀寫者問題。

讀寫者問題(第二類)

將只讀數(shù)據(jù)的進(jìn)程稱為“讀者”進(jìn)程,而寫或修改數(shù)據(jù)的進(jìn)程稱為“寫者”進(jìn)程。允許多個(gè)“讀者”同時(shí)讀數(shù)據(jù),但不允許“寫者”與其他“讀者”或“寫者”同時(shí)訪問數(shù)據(jù)。另外,要保證:一旦有“寫者”等待時(shí),新到達(dá)的“讀者”必須等待,直到該“寫者”完成數(shù)據(jù)訪問為止。試用P、V 操作正確實(shí)現(xiàn)“讀者”與“寫者”的同步。

這個(gè)問題的約束條件在第一類讀寫者問題的基礎(chǔ)上增加了一條:

  • 一旦有“寫者”等待時(shí),新到達(dá)的“讀者”必須等待,直到該“寫者”完成數(shù)據(jù)訪問為止。

這是基于“寫者優(yōu)先”的策略。寫者優(yōu)先是因?yàn)椋?dāng)共享數(shù)據(jù)區(qū)被讀者占用時(shí),緊鄰的讀者可能會立刻進(jìn)入,并且長時(shí)間讀取,而寫者相當(dāng)長的時(shí)間被阻塞無法進(jìn)入數(shù)據(jù)區(qū),從而造成“寫者饑餓”。

這個(gè)問題是過橋問題的一個(gè)變種。若將讀者、寫者分別看作是兩方過橋的人,而將共享數(shù)據(jù)區(qū)看作是一座橋(可以通過任意數(shù)目的人,即過橋問題中的N=+∞)。由此可以轉(zhuǎn)述為如下描述:

讀者、寫者分別在橋兩邊準(zhǔn)備過橋。過橋的人只能沿著橋向前走而不能向后退。對于讀者而言,只要對岸沒有寫者,就可以通過任意數(shù)目的人,但是一旦對岸有寫者準(zhǔn)備過橋,準(zhǔn)備過橋的讀者將等待寫者過橋后才能過橋。對于讀者而言,如果橋上有任何人(讀者或?qū)懻撸急仨毜人麄冞^了橋后,自己才能過橋。

大體的代碼框架沒有變化。變量和信號量需要:

  • 橋(共享數(shù)據(jù)區(qū))的互斥信號量:semDataMutex=1
  • 讀者、寫者的計(jì)數(shù)變量:readerCounter=0, writerCounter=0
  • 計(jì)數(shù)變量保護(hù)信號量:semReaderCounter=1, semWriterCounter=1

由于對于讀者而言不限制過橋人數(shù),因此沒有必要設(shè)置semMax信號量。

在過橋問題中,讀者的步驟分為三個(gè)部分:

//第一部分:準(zhǔn)備上橋
if(是第一個(gè)上橋的讀者){
    試圖獲取過橋的權(quán)力
    if(沒有成功)  進(jìn)入等待隊(duì)列
}

//第二部分:過橋
過橋

//第三部分:準(zhǔn)備下橋
if(是最后一個(gè)下橋的讀者){
    釋放橋資源
}       

可以看出,只要有一個(gè)讀者上了橋,并且橋上還有讀者,后續(xù)讀者都可以上橋。為了保證“一旦對岸有寫者準(zhǔn)備過橋,準(zhǔn)備過橋的讀者將等待寫者過橋后才能過橋”,必須對這種行為加以限制:
????考慮存在某個(gè)信號量,它指示是否有寫者準(zhǔn)備過河,每個(gè)讀者上橋之間都要檢測這個(gè)信號量,如果發(fā)生寫者準(zhǔn)備過橋的情況,它就要進(jìn)入等待隊(duì)列。顯然,對這個(gè)信號量的檢測操作必須在準(zhǔn)備上橋進(jìn)行:

if(有寫者準(zhǔn)備過河){
    進(jìn)入等待序列
}else{
    //第一部分:準(zhǔn)備上橋
}

//第二部分:過橋

//第三部分:準(zhǔn)備下橋    

對于寫者:

  • 如果他是第一個(gè)準(zhǔn)備上橋的人,那么他要發(fā)出某個(gè)信號告知對岸那些準(zhǔn)備過橋的讀者他要準(zhǔn)備過河,請讀者等待。
  • 根據(jù)寫者優(yōu)先,如果有很多個(gè)寫者準(zhǔn)備過橋,這個(gè)信號只有等最后一個(gè)寫者過完橋后才能撤銷,在此期間,等待的讀者還是在等待。
  • 因?yàn)閷懻弑仨毜葮蛏险谶^河的任何人(無論是寫者還是讀者)都過了他才能過橋(互斥),所以它要競爭過橋

因此它的步驟分為5個(gè)部分

//第一部分:發(fā)出信號
if(若是第一個(gè)上橋的寫者){
    發(fā)出準(zhǔn)備過河信號
}

//第二部分:準(zhǔn)備過橋
試圖過橋(競爭橋資源)
if(沒有成功)  //橋上還有人
    進(jìn)入等待隊(duì)列  //等待他們都通過橋

//第三部分:過橋
過橋

//第四部分:準(zhǔn)備下橋
釋放橋資源  //保證橋上最多只有一個(gè)寫者,并且只有寫者全部通過讀者才能繼續(xù)過
if(是最后一個(gè)下橋的寫者){
    撤銷寫者過河信號,等待中的讀者可以準(zhǔn)備過橋
}       

綜上所述,這個(gè)“準(zhǔn)備過河的信號”可以設(shè)為semWRMutex=1

//變量
readerCounter=0, writerCounter=0 //計(jì)數(shù)變量
//信號量
semaphore
    semDataMutex=1 //共享資源互斥
    semReaderCounter=1, semWriterCounter=1  //計(jì)數(shù)變量保護(hù)
    semWRMutex=1 //有寫者準(zhǔn)備過河

//讀者:
reader(){
    //探測是否有寫者準(zhǔn)備進(jìn)入,這里不要把P理解為wait,
    //在這里需要有一個(gè)掛入等待隊(duì)列的功能,P是合適的
    P(semWRMutex)  
        //如果沒有寫者準(zhǔn)備進(jìn)入,則semWRMutex=1,
        //P(semWRMutex) 后semWRMutex為0,然后這個(gè)讀者進(jìn)入       

        P(semReaderCounter)
            readerCounter++;
            if(readerCounter==1)
                P(semsemDataMutex)
        V(semReaderCounter)

     //在下面的語句中又立刻將semWRMutex設(shè)為1
    V(semWRMutex)

    //執(zhí)行讀操作
    /*................*/


    //準(zhǔn)備退出
    P(semReaderCounter)
        readerCounter--;
        if(readerCounter==0)
            V(semsemDataMutex)
    V(semReaderCounter)
}


//寫者
writer(){
    P(semWriterCounter)
        WriterCounter++;
        if(WriterCounter==1)
            //第一個(gè)準(zhǔn)備進(jìn)入的寫者發(fā)出準(zhǔn)備進(jìn)入信號
            //這個(gè)信號直到最后一個(gè)寫者結(jié)束后才撤銷
            //注意這里的“發(fā)出”并不是signal(V原語)
            P(semWRMutex)
    V(semWriterCounter)

    //由于互斥,必須競爭
    P(semsemDataMutex)
    //執(zhí)行寫操作
    //退出
    V(semsemDataMutex)
    
    //探測是否是最后一個(gè)寫者
    P(semWriterCounter)
        WriterCounter--;
        if(WriterCounter==0)
            //最后一個(gè)寫者結(jié)束后撤銷信號,通知相關(guān)等待讀者進(jìn)入
            V(semWRMutex)
    V(semWriterCounter)
}

倉庫問題

有一個(gè)倉庫,可以存放A 和B 兩種產(chǎn)品,但要求:
(1)每次只能存入一種產(chǎn)品(A 或B);
(2)-N<A 產(chǎn)品數(shù)量-B 產(chǎn)品數(shù)量<M。其中,N 和M 是正整數(shù)。
試用同步算法描述產(chǎn)品A 與產(chǎn)品B 的入庫過程。

對于不等式-N<A 產(chǎn)品數(shù)量-B 產(chǎn)品數(shù)量<M可以拆分為兩個(gè)不等式:B-A<NA-B<M。也就是說,B的數(shù)量至多只能比A多N-1個(gè),且A的數(shù)量至多只能比B多M-1個(gè)。
顯然,倉庫是一個(gè)臨界資源,所以需要設(shè)置一個(gè)互斥信號量semRepository=1

然而如何保證A、B的數(shù)量滿足約束條件呢?考慮到,如果A、B之間的數(shù)量差一旦超過約束范圍,比如某時(shí)刻A-B=M-1了,那么接下來A就不能再放入了,這個(gè)時(shí)候A必須進(jìn)入等待隊(duì)列等待。所以很容易想到使用信號量來保證約束條件。
分別為A、B設(shè)置約束信號量semEnableAsemEnableB,分別表示A、B還可以存入的數(shù)量,那么它們的初值是多少呢?或者說,這兩個(gè)信號量該如何使用?

考慮在任意時(shí)刻,倉庫中A、B的數(shù)量都在約束范圍內(nèi),假定現(xiàn)在要放入一個(gè)A,必須檢測這個(gè)A能否放入,即執(zhí)行P(semEnableA)操作,如果判定不能放入,則A進(jìn)入等待隊(duì)列;另外,如果判定能放入,那么A放入后,B還可存入數(shù)量也要加一個(gè)(semEnableB++),這樣雙方數(shù)量差才能不超過界限,這就要求要執(zhí)行V(semEnableB)。對于B而言也是同理。也就是:semEnableA在A進(jìn)程中被P,但是卻是在B進(jìn)程中被V。

設(shè)semEnableA初值為K。在初始時(shí)刻,倉庫是空的。考慮一個(gè)極端情況,就是只放入A而不放B,從而不斷執(zhí)行P(semEnableA)操作,semEnable不斷自減,由于B的數(shù)量始終是0,所以這個(gè)過程直到A的數(shù)量為M-1后停止,此時(shí)semEnableA=K-(M-1)。之后再放入A時(shí)執(zhí)行P(semEnableA)將導(dǎo)致A被掛起等待,這蘊(yùn)含了semEnableA=0,也就是K=M-1,所以semEnableA的初始值為M-1。對于B而言,同理可得semEnableB的初始值為N-1

由此寫出如下為偽代碼:

semaphore
    semRepository=1,
    semEnableA=M-1, semEnableB=N-1

//放入A
putA(){
    取A產(chǎn)品
    P(semEnableA) //檢測能否放入A
        P(semRepository) //若能,則鎖住倉庫
              放入A
        V(semRepository)
    V(semEnableB) //將B可放入的數(shù)量加一個(gè)
}

//放入B
putB(){
    取B產(chǎn)品
    P(semEnableB) //檢測能否放入B
        P(semRepository) //若能,則鎖住倉庫
              放入B
        V(semRepository)
    V(semEnableA) //將A可放入的數(shù)量加一個(gè)
}

多任務(wù)同步問題(狀態(tài)合并和拓?fù)渑判颍?/h3>

設(shè)有如下5個(gè)任務(wù),每個(gè)節(jié)點(diǎn)代表一個(gè)任務(wù)。對每一個(gè)任務(wù)而言,只有其所有前驅(qū)任務(wù)都被執(zhí)行完畢,該任務(wù)才能開始執(zhí)行,請使用P、V原語實(shí)現(xiàn)。

這是一個(gè)典型的進(jìn)程同步問題。一種普適的做法是為每一個(gè)箭頭標(biāo)明一個(gè)使用信號量,由于此處有7個(gè)箭頭,于是存在7個(gè)私用信號量,它們的初始值都是0

對于某個(gè)任務(wù)Ti,它的框架是:

Ti {
    對Ti的入度信號量執(zhí)行P原語
    執(zhí)行Ti
    對Ti的出度信號量執(zhí)行V原語
}

例如對于T1有:

T1{
    do T1    //由于T1沒有前驅(qū)任務(wù),所以不必執(zhí)行P原語
    V(S12)
    V(S13)
}

對于T2有:

T1{
    P(S12)
    do T2  
    V(S24)
}

對于T6

T6{
    P(S46)
    P(S56)
    do T6    //由于T6沒有后繼任務(wù),所以不必執(zhí)行V原語
}

但是實(shí)際上,可以通過合并箭頭的方式來減少信號量的數(shù)目。考慮下面兩種情況:

  1. 一對多情況

    即存在n個(gè)進(jìn)程Pik(k=1,2,...,n),它們的前驅(qū)進(jìn)程是P1。事實(shí)上可以將這n個(gè)箭頭合并為一個(gè)箭頭,從而只用設(shè)置一個(gè)信號量S1i=0

    對于P1而言,它的程序框架是:
    P1{
        /*...前驅(qū)事務(wù)代碼*/
        V(S1i)
        V(S1i)
        ...... //執(zhí)行n次V(S1i)
        V(S1i)
    }
    
    而對Pik而言,它的代碼框架是:
    Pik{
        P(S1i)
        /*...后繼事務(wù)代碼*/
    }
    
    對于其解釋是這樣的:假定在極短的時(shí)間內(nèi)進(jìn)程P1Pik(k=1,2,...,n)進(jìn)程同時(shí)執(zhí)行了。那么由于信號量S1i初始值為0,且P(S1i)被執(zhí)行n次,從而S1i=-n并且Pik(k=1,2,...,n)均被掛入等待對隊(duì)列進(jìn)行等待。然后,等到P1執(zhí)行完它的前驅(qū)事務(wù)代碼后,依次執(zhí)行n次V(S1i),每次執(zhí)行一次就會使得等待隊(duì)列中的一個(gè)進(jìn)程Pik進(jìn)入就緒隊(duì)列準(zhǔn)備執(zhí)行。由于這n個(gè)后繼進(jìn)程不在同一條鏈上,所以它們可以并發(fā)執(zhí)行,同時(shí)也滿足了同步關(guān)系。
  2. 多對一情況

    對于這種情況,我們?nèi)匀豢梢詫⒓^進(jìn)行合并,并專設(shè)一個(gè)信號量Si1=0來表示同步關(guān)系

    對于P1而言,它的程序框架是:
    P1{
        P(S1i)
        P(S1i)
        ...... //執(zhí)行n次P(Si1)
        P(S1i)
        /*...后繼事務(wù)代碼*/
    }
    
    而對Pik而言,它的代碼框架是:
    Pik{
        /*...前驅(qū)事務(wù)代碼*/
        V(Si1)
    }
    
    對于這種情況的解釋是:當(dāng)進(jìn)程P1執(zhí)行第一句P(Si1)時(shí),由于信號量Si1初值為0,從而P1進(jìn)入等待隊(duì)列,此時(shí)Si1=-1。然后,任意一個(gè)前驅(qū)進(jìn)程Pik執(zhí)行完畢后執(zhí)行V(Si1),使得Si1=0,并且P1又進(jìn)入就緒隊(duì)列準(zhǔn)備執(zhí)行。P1執(zhí)行完第二個(gè)P(Si1)后,Si1又等于-1且其自身又進(jìn)入等待隊(duì)列被阻塞,直到下一個(gè)前驅(qū)進(jìn)程Pik'執(zhí)行完畢后執(zhí)行V(Si1)使其繼續(xù)運(yùn)行……如此循環(huán)往復(fù),直到所有的這n個(gè)前驅(qū)進(jìn)程Pik都執(zhí)行完畢,進(jìn)程P1才能執(zhí)行它的后繼事務(wù)代碼,由此完成同步功能。

那么如何進(jìn)行合并呢?第k節(jié)點(diǎn)的所有入邊都標(biāo)上semIn(k)的標(biāo)記,所有出邊都標(biāo)上semOut(k)標(biāo)記。設(shè)第k個(gè)結(jié)點(diǎn)有numIn個(gè)入邊,有numOut個(gè)出邊,則其算法為:

T(k){
    P(semIn(k)) × numIn //表示依次執(zhí)行P(semIn(k))共numIn次
    do work
    V(semOut(k)) × numOut //表示依次執(zhí)行V(semIn(k))共numOut次
}

對于本題的圖:


它的信號量可以縮減為4個(gè):


所以代碼如下:

semaphore
    S1,S2,S3,S4;
S1=S2=S3=S4=0;

T1{
    do work
    V(S1) × 2
}
T2{       
    P(S1)    
    do work 
    V(S3)
}
T3{       
    P(S1)    
    do work 
    V(S2)
}
T4{
    P(S3) × 2
    do work
    V(S4)
}
T5{
    P(S2)
    do work 
    V(S3)
    V(S4)
}
T6{
    P(S4) × 2
    do work
}

其執(zhí)行過程表如下,可見是保持了同步關(guān)系,并且信號量只需要四個(gè)而不是七個(gè):


吃水果問題(簡單)

桌子上有一只盤子,盤子中只能放一只水果。爸爸專向盤子中放蘋果,媽媽專向盤子中放橘子,一個(gè)兒子專等吃盤子中的橘子,一個(gè)女兒專等吃盤子中的蘋果。用PV 操作實(shí)現(xiàn)他們之間的同步機(jī)制。

由于不能同時(shí)放蘋果和橘子,所以盤子是一個(gè)臨界資源,而父親母親就是互斥關(guān)系。如果盤子中沒有水果時(shí),兒子女兒是不能取水果的,所以父親母親和女兒兒子之間是同步關(guān)系。所以,思路如下:

  • 父親母親對盤子信號量操作,獲得放水果的許可
  • 父親(母親)放了水果后,發(fā)出私用信號量來通知女兒(兒子)取水果
  • 女兒(兒子)取完水果后通知父親(母親)放水果,并釋放盤子資源

由于這個(gè)盤子中只能放一個(gè)水果,所以父親與女兒、母親與兒子之間各只需一個(gè)私用信號量。

這個(gè)題的核心思想在于其同步過程實(shí)現(xiàn):父親與女兒是同步的,父親鎖住的盤子只有女兒才能釋放。母親與兒子是同步的,母親鎖住的盤子只有兒子才能釋放。而盤子的互斥信號量又是公用的。
這就保證了:父親放了水果后,除非女兒進(jìn)程拿了水果并釋放盤子,否則其他進(jìn)程都要等待。即父親母親獲取不到盤子而等待,兒子因?yàn)楸P中沒有橘子的信號(母親沒有更改semOrange仍然是0)而陷入等待。

semaphore
    semPlate=1 //盤子的互斥信號量
    semApple=0, semOrange=0 //注意同步進(jìn)程的信號量初始值是0

//父親放蘋果
putApple(){
    //獲得盤子,它的釋放是在取蘋果進(jìn)程中進(jìn)行的
    //P(semPlate) 后,除非女兒進(jìn)程去了蘋果釋放盤子
    //否則父親。母親、兒子三個(gè)進(jìn)程都取不到盤子
    P(semPlate) 
        放蘋果
        V(semApple) //通知女兒吃蘋果,由于semApple初始值0,V原語后變?yōu)?
                    //此后,當(dāng)女兒還沒有取到蘋果并釋放盤子時(shí),下一輪放蘋果
                    //操作會在P(semPlate)這一步被掛起等待
}

//女兒取蘋果
getApple(){
    //接收到取蘋果信號。該操作使得semApple從1又變?yōu)?
    //從而如果父親未放蘋果,semApple始終是0,則在下一輪取蘋果操作時(shí)
    //在這一步被掛起等待
    P(semApple) 
        取蘋果
        V(semPlate) //釋放盤子資源
}

//取橘子和拿橘子進(jìn)程如法炮制
putOrange(){
    /*.......*/
}

getOrange(){
    /*.......*/
}

吃水果問題(生產(chǎn)者-消費(fèi)者)

桌子上有一只盤子,盤子中最多放N個(gè)水果。爸爸專向盤子中放蘋果,媽媽專向盤子中放橘子,且每次只能放一個(gè)。兒子女兒每次可以拿一個(gè)水果來吃。用PV 操作實(shí)現(xiàn)他們之間的同步機(jī)制。

這是上個(gè)吃水果問題的一個(gè)變種。它的變化是:

  1. 盤子(緩沖區(qū))的容量是N
  2. 生產(chǎn)者生產(chǎn)的東西消費(fèi)者都可以使用,女兒可以吃蘋果橘子,兒子也可以。

在上個(gè)簡單的吃水果問題中,父親鎖住盤子后放了蘋果,當(dāng)且僅當(dāng)它的同步進(jìn)程——女兒進(jìn)程取完蘋果后釋放盤子資源,否則其他所有進(jìn)程都會進(jìn)入等待。這就保證了父親女兒同步進(jìn)行。

而在這個(gè)問題中,盤子可放的水果不止一個(gè),生產(chǎn)者進(jìn)程鎖住盤子后放了水果,但是不應(yīng)該在消費(fèi)者進(jìn)程釋放盤子,否則生產(chǎn)者放一個(gè)水果,當(dāng)且僅當(dāng)消費(fèi)者拿走后才能放下一個(gè)水果,這樣盤子任意時(shí)刻至多只能有一個(gè)水果,這顯然是不合理的。無論是生產(chǎn)者還是消費(fèi)者,在對盤子操作之前一個(gè)鎖住盤子,而在操作完成后要自己釋放盤子。

盤子設(shè)置一個(gè)互斥信號量semPlate=1,由于盤子最多可以放N個(gè)水果,所以信號量要增設(shè)兩個(gè):目前可以取多少個(gè)水果semEnableGet,以及目前有多少個(gè)空位可以放水果semEnablePut。初始時(shí),有N個(gè)位置可以放水果,而有0個(gè)水果可以取,所以初始值semEnableGet=0semEnablePut=N
對這兩個(gè)信號量的操作是這樣的:

  1. 生產(chǎn)者檢測是否可以放水果(P(semEnablePut))。如果可以就放(semEnablePut也就減少一個(gè)空位),否則要等待空位。
  2. 得到放置許可后,生產(chǎn)者要鎖住盤子(P(semPlate))
  3. 然后放水果
  4. 生產(chǎn)者釋放盤子(V(semPlate)),并且通知可以取水果了(V(semEnableGet)后semEnableGet自增1,表示多一個(gè)水果可取)
  1. 消費(fèi)者檢測是否有水果可以取(P(semEnableGet))。如果可以就取(semEnableGet也就減少一個(gè)水果),否則要等待水果。
  2. 得到放置許可后,消費(fèi)者要鎖住盤子(P(semPlate))
  3. 然后取水果
  4. 消費(fèi)者釋放盤子(V(semPlate)),并且通知可以放水果了(V(semEnablePut)后semEnablePut自增1,表示多一個(gè)空位可以放水果)

注意,V原語次序可以任意,但是P原語順序不能亂,否則可能會引發(fā)死鎖。

哲學(xué)家問題:

假設(shè)有五位哲學(xué)家圍坐在一張圓形餐桌旁,做以下兩件事情之一:吃飯,或者思考。吃東西的時(shí)候,他們就停止思考,思考的時(shí)候也停止吃東西。餐桌中間有一大碗意大利面,每兩個(gè)哲學(xué)家之間有一只筷子,哲學(xué)家必須用兩只筷子吃東西。他們只能使用自己左右手邊的那兩只筷子。條件是:
1. 只有拿到左右兩只筷子才吃飯
2. 如果筷子在別人手上,必須等他人吃完后才能拿筷子
3. 在未拿到兩只筷子吃飯前,絕對不會放下自己手中的筷子

描述一個(gè)保證不會有兩個(gè)相鄰的人同時(shí)要求吃飯的通信算法是簡單的:

semaphore 
    chopstick[0]~chopstick[4]=1    //5根筷子的互斥信號量
philosopher(i){  //第i個(gè)哲學(xué)家進(jìn)程
    P(chopstick[i]); //拿左邊的筷子
        P(chopstick[i+4 mod 5])  //拿右邊的筷子,注意mod的使用
            吃飯
        V(chopstick[i+4 mod 5])  //放下右邊的筷子
    V(chopstick[i]); //放下左邊的筷子
}

盡管這個(gè)算法是簡單的,但是它有一個(gè)潛在的隱患:可能會引發(fā)死鎖。假設(shè)5個(gè)哲學(xué)家同時(shí)拿起左手邊的筷子,但是都沒有右邊的筷子,且根據(jù)題意,他們都不會放下自己的筷子,從而沒有人能吃飯。

為了避免死鎖,改進(jìn)上述算法。因?yàn)樗腥硕家饶米约鹤笫诌叺目曜樱@樣可能每個(gè)人手中都恰好有一根筷子,這是引發(fā)死鎖的關(guān)鍵。
自然想到,如果兩個(gè)相鄰的哲學(xué)家都首先取第一根筷子既可避免。讓偶數(shù)編號的哲學(xué)家先拿左手邊的筷子(第i個(gè)哲學(xué)家拿第i個(gè)筷子)而讓奇數(shù)編號的哲學(xué)家拿右手邊的筷子(第i個(gè)哲學(xué)家拿第 (i+4)mod5 個(gè)筷子):


先拿的筷子

這樣,任何一個(gè)哲學(xué)家拿到一根筷子時(shí),就會阻止他相鄰的哲學(xué)家拿到該筷子,這名哲學(xué)家從而只能等待:

semaphore 
    chopstick[0]~chopstick[4]=1    //5根筷子的互斥信號量
philosopher(i){  //第i個(gè)哲學(xué)家進(jìn)程
    if(i是偶數(shù))
        P(chopstick[i]); //拿左邊的筷子
            P(chopstick[i+4 mod 5])  //拿右邊的筷子,注意mod的使用
                吃飯
            V(chopstick[i+4 mod 5])  //放下右邊的筷子
        V(chopstick[i]); //放下左邊的筷子
    else
        P(chopstick[i+4 mod 5]); //拿右邊的筷子
            P(chopstick[i])  //拿左邊的筷子
                吃飯
            V(chopstick[i+4 mod 5])  //放下右邊的筷子
        V(chopstick[i]); //放下左邊的筷子
}

讀卡機(jī)問題

讀卡機(jī)上讀卡片。這一項(xiàng)工作由三個(gè)進(jìn)程get,copy和put以及兩個(gè)緩沖區(qū)buffer1和 buffer2 完成。進(jìn)程get的功能是把一張卡片上的信息從讀卡機(jī)上讀進(jìn)buffer1;進(jìn)程copy的功能是把buffer1中的信息復(fù)制到buffer2;進(jìn)程put的功能是取出buffer2中的信息并從行式打印機(jī)上打印輸出。

這是一個(gè)同步問題。有一個(gè)錯(cuò)誤的解法是這樣的:

因?yàn)間et和copy、copy和put之間都是同步關(guān)系。如果buffer1中g(shù)et沒有放入數(shù)據(jù),copy是取不到數(shù)據(jù)的,只能等待。對于buffer2也同理。所以:

//錯(cuò)誤做法: ERROR SOLUTION
semaphore
    enable1=0,enable2=0; //兩個(gè)緩沖區(qū)是否可以放入數(shù)據(jù)
get{
    放入數(shù)據(jù);
    V(enable1)
}
copy{
    P(enable1)
        取數(shù)據(jù)
        放入buffer2
        V(enable2)
}
put{
    P(enable2)
    取數(shù)據(jù)
}

乍一看似乎是合理有效的:一開始都沒有數(shù)據(jù),enable1和enable2都是0,copy在運(yùn)行 P(enable1)時(shí)會進(jìn)入等待,put在運(yùn)行P(enable2)也會進(jìn)入等待。直到get放入數(shù)據(jù)并執(zhí)行 V(enable1)時(shí),copy才被激活,然后copy執(zhí)行 V(enable2)時(shí),put才被激活取數(shù)據(jù)。此后get不再放入數(shù)據(jù),copy和put繼續(xù)等待,如此往復(fù)。

但是這里有一個(gè)重大隱患,那就是沒有考慮到兩個(gè)緩沖區(qū)是臨界資源沒有被保護(hù)。你可能會想,get和copy是同步關(guān)系而 不是互斥關(guān)系 ,如果get不通知copy來去數(shù)據(jù),那么copy就會在P(enable1)這一步時(shí)陷入等待,根本不會訪問buffer1,似乎臨界資源不產(chǎn)生沖突。

但是恰恰忽略的一點(diǎn)是,copy在取數(shù)據(jù)時(shí),因?yàn)閎uffer1沒有被保護(hù)而使得copy也在往里面填充數(shù)據(jù)。這就會引發(fā)臟數(shù)據(jù)。所以盡管時(shí)同步,也要對臨界資源進(jìn)行互斥:

semaphore
    enable1=0,enable2=0, //兩個(gè)緩沖區(qū)是否可以放入數(shù)據(jù)
    bufMutex1=1,bufMutex2=1; //兩個(gè)緩沖區(qū)的互斥信號量
get{
    P(bufMutex1)
        放入數(shù)據(jù);
    V(bufMutex1)

    V(enable1)
}
copy{
    P(enable1)
        P(bufMutex1)
            取數(shù)據(jù)
        V(bufMutex1)

        P(bufMutex2)
            放入buffer2
        V(bufMutex2)

        V(enable2)
}
put{
    P(enable2)
    
    P(bufMutex2)
        取數(shù)據(jù)
    V(bufMutex2)
}

這樣在copy取buffer1中數(shù)據(jù)時(shí),get需要等待而不能放入數(shù)據(jù)。

注意到,enable1這個(gè)信號量在這里僅表示能否裝入,所以只適合表示緩沖區(qū)僅能放置一份數(shù)據(jù)的情況。
但是假如是一個(gè)緩沖隊(duì)列,里面可以放置N份數(shù)據(jù),那么只使用enable1就明顯不夠了,為此需要設(shè)置兩個(gè)信號量:

  • 當(dāng)前還能放多少數(shù)據(jù):enablePutData=N(相當(dāng)于empty)
  • 當(dāng)前有多少份數(shù)據(jù)可以取:enableGetData=0(相當(dāng)于full)

enablePutData初始值是N,表示剛開始可以放N份數(shù)據(jù);enableGetData初始值是0,表示剛開始沒有數(shù)據(jù)可以取:

  • 每放一份數(shù)據(jù),執(zhí)行P(enablePutData)來使enablePutData自減1,表示少一個(gè)單元可放數(shù)據(jù),如果enablePutData等于0,表示沒有單元可以放數(shù)據(jù),則生產(chǎn)者需要等待。
    放完數(shù)據(jù)后,需要執(zhí)行V(enableGetData)來使enableGetData自增1,表示緩沖區(qū)多一個(gè)數(shù)據(jù)可取,就是剛剛放入的那一份。
  • 每取一份數(shù)據(jù),執(zhí)行P(enableGetData)來使enableGetData自減1,表示少一個(gè)數(shù)據(jù)可取,就是剛剛?cè)×说哪且环荩绻鹐nableGetData等于0,表示沒有數(shù)據(jù)可取,則消費(fèi)者需要等待。
    取完數(shù)據(jù)后,需要執(zhí)行V(enablePutData)來使enablePutData自增1,表示緩沖區(qū)多一個(gè)空位可以放數(shù)據(jù)。

所以拓展后可以寫為:

semaphore
    enablePutData1=N,enablePutData2=N,
    enableGetData1=0,enableGetData2=0,
    bufMutex1=1,bufMutex2=1; //兩個(gè)緩沖區(qū)的互斥信號量
get{
    //先測試能否放數(shù)據(jù)再互斥臨界區(qū),P原語順序錯(cuò)亂可能會引發(fā)死鎖
    P(enablePutData1)
        P(bufMutex1)
            放入數(shù)據(jù);
        V(bufMutex1)
    V(enableGetData1)
}
copy{
    P(enableGetData1) //測試buffer1能不能取數(shù)據(jù)
        P(bufMutex1)
            取數(shù)據(jù)
        V(bufMutex1)
    V(enablePutData1)

    //測試buffer2能不能放數(shù)據(jù)
    P(enablePutData2)
        P(bufMutex2)
            放入buffer2
        V(bufMutex2)
     V(enableGetData2)
}
put{
    //測試buffer2能不能取數(shù)據(jù)
    P(enableGetData2)
        P(bufMutex2)
            取數(shù)據(jù)
        V(bufMutex2)
     V(enablePutData2)
}

如果N=1就是本題的情況,測試enable信號量可以減少為1個(gè)。

游覽車問題

已知風(fēng)景區(qū)內(nèi)的轎車總量為M輛,游客總數(shù)為N,約定:
(1)每輛轎車限乘一位游客;
(2)如果有空閑的轎車,應(yīng)當(dāng)允許想游覽的游客乘坐;
(3)無空閑轎車時(shí),游客只能排隊(duì)等待;
(4)若沒有想游覽的游客,空閑的轎車也要等待。

顯然存在一個(gè)轎車進(jìn)程Car()和游客進(jìn)程Passager()。題目中的M、N在這里并不是想要告訴你車數(shù)和人數(shù),而是想告訴你車輛數(shù)目和游客數(shù)目是固定的。這里的“固定”指游客游覽完可以繼續(xù)游覽,轎車游覽完可以繼續(xù)拉客。所以,本題的相關(guān)信號量的值和M、N都沒有關(guān)系!

這道題巧用了信號量和等待機(jī)制。首先用自然語言表述出這兩個(gè)進(jìn)程要干什么。

對于轎車進(jìn)程Car(),每當(dāng)它運(yùn)行時(shí):

  1. 可用轎車數(shù)目加1,通知(喚醒)等待的游客。
  2. 在游客進(jìn)程沒有發(fā)出發(fā)信號表明上車可以出發(fā)之前,轎車是在等待狀態(tài)。
  3. 帶上乘客游覽,在這一段時(shí)間內(nèi),游客進(jìn)程什么也不做,從而處于等待狀態(tài)
  4. 發(fā)出游覽完成信號,通知(喚醒)乘客下車
  5. 在接收到游客下車完成信號之前,一直等待游客下車。然后返回第一步。

對于游客進(jìn)程Passager(),每當(dāng)它運(yùn)行時(shí):

  1. 檢查是否有可用轎車(轎車有可用信號),如果有則選定一個(gè)轎車,否則等待
  2. 開始上車,在這個(gè)過程中,轎車進(jìn)程等待
  3. 上車后,發(fā)出開車信號,喚醒轎車進(jìn)程出發(fā)
  4. 游覽過程中游客不需要做什么,這就是處于等待狀態(tài),需要轎車進(jìn)程發(fā)出游覽完成信號后,游客進(jìn)程才從等待狀態(tài)喚醒,執(zhí)行下一步
  5. 游客下車,在此過程中,轎車進(jìn)程等待
  6. 下車完成后,發(fā)出下車完成信號,本次游覽結(jié)束,游客繼續(xù)返回第一步準(zhǔn)備游覽。
虛線代表等待狀態(tài),圓角框代表運(yùn)行狀態(tài)

上述描述中可以學(xué)到如下技巧:

  1. A進(jìn)程與B進(jìn)程交互運(yùn)行時(shí),當(dāng)某個(gè)操作要在A進(jìn)程完成,此間B進(jìn)程陷入等待,直到A進(jìn)程完成后發(fā)出信號激活B進(jìn)程。為此,設(shè)置一個(gè)信號量semaphore=0,B中執(zhí)行P(semaphore)使得B等待,而當(dāng)A完成操作后,執(zhí)行V(semaphore)來喚醒B進(jìn)程,因?yàn)锽進(jìn)程掛在semaphore的等待隊(duì)列中。
  2. 游客進(jìn)程通過對可用車數(shù)semEnableCar進(jìn)行P操作后,如果沒有可用的轎車,就掛入該信號量等待隊(duì)列中。而后無論來多少輛車,因?yàn)檐囈暶骺捎密嚁?shù)加1,也就是執(zhí)行V操作,這就會喚醒semEnableCar等待隊(duì)列中的游客進(jìn)程,讓游客上車。
    反過來說,如果轎車進(jìn)程執(zhí)行V操作后,如果沒有游客進(jìn)程,那么就不可能收到開車信號,也就會陷入等待中。
    所以,semEnableCar的初始值就是0,而不是M或者N什么的。當(dāng)沒有一個(gè)車時(shí),游客的P(semEnableCar)才能導(dǎo)致游客等待。
semaphore
    semEnableCar=0,   //可用車數(shù)目信號量
    semCanStart=0,    //可以開車信號量
    semFinish=0,    //游覽完成信號量
    semHasTakenOff=0    //游客已經(jīng)下車信號量

Car(){
    V(semEnableCar)  //可用車+1,通知等待的游客
    P(semCanStart) //等待游客的“可以出發(fā)”信號
        開始游覽,在這個(gè)過程中,游客正在等待,也就是阻塞狀態(tài)
    V(semFinish)  //向游客進(jìn)程發(fā)出游覽完成信號
    P(semHasTakenOff)   //等待游客的“已經(jīng)下車”信號
    Car() //循環(huán)運(yùn)行
}

Passager(){
    P(semEnableCar)  //等待有可用車
        上車,此時(shí)轎車等待
    V(semCanStart) //發(fā)出“可以出發(fā)”信號
    P(Finish)  //在收到轎車“游覽完成”信號之前,進(jìn)入阻塞狀態(tài),表示“正在游覽”
        游客開始下車,此間轎車正在等待
    V(semHasTakenOff)   //游客“已經(jīng)下車”信號給轎車,激活轎車
    Passager() //循環(huán)運(yùn)行
}

理發(fā)問題

有一個(gè)理發(fā)師,一把理發(fā)椅和N把供等候理發(fā)的顧客坐的椅子。如果沒有顧客,則理發(fā)師等待;當(dāng)一個(gè)顧客到來時(shí),喚醒理發(fā)師進(jìn)行理發(fā)。如果理發(fā)師正在理發(fā)時(shí)又有新顧客到來,有空椅子可坐,他就坐下來等,如果沒有空椅子,就立即離開。為理發(fā)師和顧客各編一段程序描述他們的行為,要求不能帶有競爭條件。

這是一個(gè)比較復(fù)雜的進(jìn)程同步問題。需要設(shè)計(jì)兩個(gè)進(jìn)程:

  • 顧客進(jìn)程Customer()
  • 理發(fā)師進(jìn)程Barber()

需要注意到,在顧客與理發(fā)師發(fā)生同步交互之前,顧客需要先判斷是否需要等待(這個(gè)過程,理發(fā)師進(jìn)程不需要介入)。其邏輯如下:

等待過程

首先要解決的一個(gè)問題是,如何判斷是否有等待位置。很容易想到設(shè)置一個(gè)信號量來指示是否有等待位置,但是這么做是不合理的,理由如下:
假設(shè)存在了這么一個(gè)信號量,那么第一個(gè)顧客來到就要進(jìn)行P操作而陷入等待(V操作顯然是不合理的,因?yàn)榈谝粋€(gè)顧客來之前不可能有顧客在等待)。同時(shí)還必須通知理發(fā),否則所有顧客將無限期等待下去。
一種情況是理發(fā)師通知等待的顧客理發(fā),為此理發(fā)師進(jìn)程必須對這個(gè)信號量進(jìn)行V操作,考慮到在第一個(gè)顧客尚未來到之前,不可能有顧客在等待,所以V操作將變得沒有意義。
另一種情況是等待區(qū)顧客通知理發(fā)師理發(fā),為此必須設(shè)置另外一個(gè)信號量作為通知使用。但是由于顧客已經(jīng)陷入等待,所以附設(shè)的通知信號量將無法被顧客發(fā)出,理發(fā)師也將永遠(yuǎn)收不到通知信號。
此外,單一的通知信號量將起不到約束顧客等待數(shù)量在N之內(nèi)的作用。
綜上所述,設(shè)置一個(gè)信號量只是是否有等待位置是不合理的。

那么如何做呢?可以設(shè)置一個(gè)變量waitingNumbers,當(dāng)一個(gè)顧客來到時(shí)先判斷等待人數(shù)waitingNumbers是否超過可等待人數(shù)N,如果是則離開(即顧客進(jìn)程結(jié)束),否則waitingNumbers++,顧客準(zhǔn)備等待。顯然,所有顧客都要對waitingNumbers操作,從而waitingNumbers也就成為一個(gè)臨界資源,為此要進(jìn)行保護(hù),故而設(shè)置互斥量semWaitingNumbersMutex=1,因?yàn)橥粫r(shí)間至多只能由一個(gè)顧客進(jìn)行操作。

現(xiàn)在如果顧客進(jìn)程還沒有退出,那么他就要與理發(fā)師進(jìn)程發(fā)生交互。


進(jìn)程交互階段

注意,在這一階段有兩個(gè)關(guān)鍵的等待過程。當(dāng)顧客尚未到來或者還沒有發(fā)出準(zhǔn)備理發(fā)的信號之前,理發(fā)師是在等待的。另一方面,如果客戶沒有離開,而理發(fā)師沒有對其發(fā)出可以理發(fā)信號時(shí),客戶就需要等待,這個(gè)等待就是題目中“坐在等待區(qū)椅子上”的等待情況。

也就是說,顧客發(fā)信號喚醒等待的理發(fā)師,理發(fā)師發(fā)信號喚醒等待的顧客(★★★核心思想

針對理發(fā)師的等待,可以設(shè)置一個(gè)信號量semCustomers=0。理發(fā)師對其進(jìn)行P操作(wait),使得semCustomers為-1,從而理發(fā)師進(jìn)入等待。直到顧客對其進(jìn)行V(signal)操作,使得semCustomers為0,重新喚醒理發(fā)師進(jìn)程。

這就又引發(fā)一個(gè)問題:如果第二個(gè)顧客、第三個(gè)……第K個(gè)顧客依次到來(且沒有離開,K≤N),那么他們每個(gè)人都要進(jìn)行V(semCustomers)操作,因?yàn)榈谝粋€(gè)顧客操作后semCustomers等于0,那么后續(xù)這K人的V操作將會使得semCustomers等于K,并且因?yàn)闆]有等待進(jìn)程(針對信號量semCustomers的唯一可以等待的只有理發(fā)師進(jìn)程),所以這些顧客進(jìn)程將繼續(xù)后續(xù)操作。【V原語妙用】

在后續(xù)操作中必須使這些人陷入等待,設(shè)置一個(gè)信號量semBarber=0。所有人在V(semCustomers)后必須進(jìn)行P(semBarber),即等待理發(fā)師發(fā)出可以理發(fā)通知,即等待理發(fā)師的V(semBarber)操作。第一個(gè)顧客在收到理發(fā)師通知進(jìn)行理發(fā)時(shí),semBarber又重新歸零,這樣后來的K個(gè)人對其P操作后使得semBarber等于-1、-2、……、-K,這K個(gè)人也就進(jìn)入了等待

現(xiàn)在,論證上述過程的可持續(xù)性:當(dāng)?shù)谝晃活櫩碗x開后,理發(fā)師進(jìn)程又重新進(jìn)行P(semCustomers)操作,因?yàn)榇藭r(shí)semCustomers為K,所以理發(fā)師不會等待,而是繼續(xù)執(zhí)行。理發(fā)師接下來執(zhí)行V(semBarber)喚醒顧客,由于semBarber為-K,K個(gè)人都掛在semBarber信號量的等待隊(duì)列中,所以任選一個(gè)喚醒,進(jìn)行理發(fā)。由此可以循環(huán)執(zhí)行。

int waitingNumbers=0;  //等待的顧客
semaphore
    semWaitingNumbersMutex=1,
    semBarber=0,
    semCustomers=0,
    semCut=0,  //可以開始理發(fā)
    semFinish=0,  //理發(fā)完成

Customer(){
    P(semWaitingNumbersMutex)
        if(waitingNumbers>N)
            V(semWaitingNumbersMutex)  //此后,進(jìn)程結(jié)束,表示離開
        else{
            waitingNumbers++;
            V(semWaitingNumbersMutex);

            V(semCustomers);  //通知理發(fā)師有顧客
            P(semBarber)//等待理發(fā)師通知
            
            //接到了通知,就坐理發(fā),注意等待座位要空出來
            P(semWaitingNumbersMutex)
                waitingNumbers--;
            V(semWaitingNumbersMutex)

            //告訴理發(fā)師可以開始理發(fā)
            V(semCut)
            //等待理發(fā)完成信號
            P(semFinish)
        }
}

Barber(){
    P(semCustomers)  //等待顧客到來
    V(semBarbers)  //喚醒等待的顧客
    P(semCut)  //等待顧客可以開始理發(fā)信號
        //理發(fā)
    V(semFinish)  //理發(fā)完成
}

實(shí)際上可以推廣為M個(gè)理發(fā)師理發(fā),而執(zhí)行代碼并不發(fā)生變化。這是因?yàn)樵谙到y(tǒng)中運(yùn)行著M個(gè)Barber()進(jìn)程,它們剛開始都執(zhí)行第一個(gè)P(semCustomers)語句而陷入等待,直到有顧客到來執(zhí)行V(semCustomers)而喚醒任意一個(gè)理發(fā)師理發(fā)。任意一個(gè)理完頭發(fā)的理發(fā)師發(fā)信號來喚醒下一個(gè)等待的顧客。

上機(jī)問題

某高校計(jì)算機(jī)系開設(shè)有網(wǎng)絡(luò)課并安排了上機(jī)實(shí)習(xí),假設(shè)機(jī)房共有2m臺機(jī)器,有2n 名學(xué)生選該課,規(guī)定:
① 每兩個(gè)學(xué)生組成一組,各占一臺機(jī)器,協(xié)同完成上機(jī)實(shí)習(xí);
② 只有一組的兩個(gè)學(xué)生到齊,并且此時(shí)機(jī)房有空閑機(jī)器時(shí),該組學(xué)生才能進(jìn)入機(jī)房;
③ 上機(jī)實(shí)習(xí)由一名教師檢查,檢查完畢,一組學(xué)生同時(shí)離開機(jī)房。
試用P、V操作模擬上機(jī)實(shí)習(xí)過程。

在這里,除了學(xué)生進(jìn)程Student()和教師進(jìn)程Teacher()之外,還應(yīng)該有一個(gè)進(jìn)程,用以實(shí)現(xiàn)第一個(gè)約束條件——即兩個(gè)學(xué)生到齊后才允許進(jìn)入,不妨將該進(jìn)程命名為Monitor()。顯然教師進(jìn)程不具有此功能,它只是用來完成第三約束條件,即“檢查”功能。

可以設(shè)置一個(gè)信號量semStudent=0,當(dāng)來一個(gè)學(xué)生時(shí)執(zhí)行V操作,通知Monitor,而Monitor執(zhí)行P操作來接收通知信號量。為了實(shí)現(xiàn)“兩個(gè)學(xué)生到齊才能進(jìn)去”,Monitor需要執(zhí)行兩次P(semStudent),然后才發(fā)出兩次允許進(jìn)入信號——分別給兩個(gè)申請進(jìn)入的學(xué)生使用。其核心的步驟是:

  1. 初始,semStudent=0,
  2. Monitor執(zhí)行第一個(gè)P(semStudent) => semStudent=-1,陷入等待
  3. 第一個(gè)學(xué)生執(zhí)行V(semStudent) => semStudent=0,喚醒Monitor,該學(xué)生等待接收可進(jìn)入信號。
  4. Monitor執(zhí)行第二個(gè)P(semStudent) => semStudent=-1,陷入等待
  5. 第二個(gè)學(xué)生執(zhí)行V(semStudent) => semStudent=0,喚醒Monitor,該學(xué)生等待接收可進(jìn)入信號。
  6. Monitor繼續(xù)執(zhí)行,分別執(zhí)行兩次V(semEnter) => 兩個(gè)等待的學(xué)生接收到信號,進(jìn)入。

后面的步驟就很簡單了,因?yàn)楸仨氂锌臻e的電腦,否則學(xué)生等待,因?yàn)殡娔X總數(shù)固定,所以可以設(shè)置一個(gè)電腦的信號量semComputer=2M。教師和學(xué)生進(jìn)程之間是互相同步的關(guān)系。

BEGIN
    Semaphore: 
        semStudent=0, semComputer=2M ,semEnter=0, semFinish=0, semCheck=0;
COBEGIN
    Process Procedure  semStudent()
    begin
        V(semStudent);                  // 表示有學(xué)生到達(dá)
        P(semComputer);                 // 等待獲取一臺計(jì)算機(jī)
        P(semEnter);                        // 等待進(jìn)入許可
            DO it with partner();
        V(semFinish);                       // 實(shí)習(xí)完成
        P(semCheck);                        // 等待老師檢查
        V(semComputer):                 // 釋放計(jì)算機(jī)資源
    end

Process Procedure Teacher()
    begin
        P(semFinished);                 // 等待學(xué)生實(shí)習(xí)完成
        P(semFinished);                 // 等待另一學(xué)生實(shí)習(xí)完成
            check the work();
        V(semCheck);                        // 表示檢查完成
        V(semCheck);                        // 表示檢查完成
    end

Process Procedure Monitor()
    begin
        P(semStudent);                      // 等待學(xué)生到達(dá)
        P(semStudent);                  // 等待另一學(xué)生到達(dá)
        V(semEnter);                        // 允許學(xué)生進(jìn)入
        V(semEnter);                        // 允許學(xué)生進(jìn)入
    end
    
Coend
END
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 又來到了一個(gè)老生常談的問題,應(yīng)用層軟件開發(fā)的程序員要不要了解和深入學(xué)習(xí)操作系統(tǒng)呢? 今天就這個(gè)問題開始,來談?wù)劜?..
    tangsl閱讀 4,172評論 0 23
  • 這個(gè)不錯(cuò)分享給大家,從扣上看到的,就轉(zhuǎn)過來了 《電腦專業(yè)英語》 file [fail] n. 文件;v. 保存文...
    麥子先生R閱讀 6,611評論 5 24
  • 第一百一回和第一百二回,司馬懿和諸葛亮兩人的較量仍在繼續(xù),戰(zhàn)爭打了很多年,諸葛亮五出祁山,未得寸土。其間還折損了張...
    楚歌兒閱讀 604評論 0 0
  • —— Kurny 風(fēng)拂幡動雀聲寂,月隱光息水星朦。 一點(diǎn)青影明去路,千卷墨云繞孤城。 (作于20...
    Kurny91閱讀 152評論 0 1
  • 《安靜的力量》,皮克·耶爾作品。 這本書是從圖書館借的,與其他成功之道和心靈雞湯一起,安靜地?cái)[在那,顯得是那么微不...
    王書劍閱讀 603評論 0 7