【操作系統】 Operation System 第十章:信號量和管程

以下內容全部是B站 的陳老師視頻的課程總結,https://www.bilibili.com/video/av6538245?p=73,感謝UP主感謝陳老師,由於第十章是最難的部分之一,我從這一章開始來對計算機專業核心課程之一的《操作系統》進行知識點總結,順序和內容全部根據B站視頻來。一方面是加深自己的印象,另一方面貢獻給大家減少大家的工作量。
內容上增加了對課程上代碼的解讀,同時手打記錄了課件上出現的全部文字。下一次更新會更新第九章內容。

10.1 信號量和管程

10.1.1 爲什麼需要信號量

回顧一下lock能解決併發問題中競爭條件(競態條件)對資源的爭奪;
但是lock不能解決同步問題,需要更高級的方式實現同步(包括多線程共享公共數據的協調執行;互斥與條件同步的實現(互斥是指同一時間只能有一個線程可以執行臨界區));
同步的實現需要高層次的編程抽象(例如鎖)的實現和底層硬件支持。
在這裏插入圖片描述

10.2 信號量

信號量是一種抽象的數據類型,包括:
->一個整形sem,兩個原子操作;
->P():sem-1,如果sem<0,等待,否則繼續
->V():sem+1,如果sem<=0,喚醒一個等待的P

信號量有點像鐵路的信號燈
在這裏插入圖片描述

信號量是Dijkstra在20世紀60年代提出,V:Verhoog,P:Prolaag,分別是荷蘭語的增加和減少。

10.3 信號量的使用

10.3.1 信號量的性質

信號量必須是整數,初始值一般是大於0;
信號量是一種被保護的變量(初始化後,唯一改變一個信號量值的方法只能是P()和V();操作只能是原子操作);
P()能阻塞,V()不會阻塞;
信號量假定是公平的(FIFO;如果一直有V()操作,則不會有進程被P()所阻塞);

信號量包括兩種類型:
->二進制信號量:0或1;
->計數信號量:任何非負整數;
->可以用上面這兩種類型任意一個表示另一個。

信號量可以用在2個方面:
->互斥
->條件同步(採用調度約束實現一個線程等待另一個線程的事情發生)

10.3.2 用二進制信號量實現lock功能,也就是互斥

在這裏插入圖片描述
信號量初始值設置爲1,一個線程開頭信號量減一鎖上,完事後加一解鎖;想一想,如果別的線程也想執行,當他執行P操作的時候,信號量爲負數了,此時其它的線程就不得不等待,等到當前線程執行完後V操作了,另一個線程才能執行。

10.3.3 用二進制信號量實現線程同步(調度約束)

在這裏插入圖片描述
信號量初始值爲0,線程A執行到P()時,信號量爲負數掛起,只有到線程B執行到V()時,線程A纔可能繼續。這確保了同步。

10.3.4 同步問題出現的場景

一個線程等待另一個線程處理事情,例如生產者-消費者模型,此時互斥(鎖機制)是不夠的;
例如生產者-消費者模型就需要一個有界緩衝區,
->一個或多個生產者產生數據並將數據放在緩衝區中;
->單個消費者每次從緩衝區取出數據;
->在任何一個時間只有一個生產者或消費者可以訪問緩衝區,如下圖。
在這裏插入圖片描述

10.3.5 生產者-消費者模型的正確性要求

->在任何一個時間只能有一個線程操作緩衝區(互斥);
->當緩衝區爲空時,消費者必須等待(調度/同步約束);
->當緩衝區滿了時,生產者必須等待(調度/同步約束);

10.3.6 生產者-消費者模型實現策略

->利用一個二進制信號量實現互斥,也就是鎖的功能;
->用一個計數信號量fullbuffers來約束生產者;
->用一個計數信號量emptybuffers來約束消費者;
在這裏插入圖片描述
Deposit():
往緩衝區增加東西。所以說fullBuffer加1,emptybuffer減1.
Remove():
和上一個相反。
可以發現,這個鎖是緊跟着對緩衝區的操作的。

10.4 信號量的實現

在這裏插入圖片描述
注意,sem>0,說明計算機能滿足所有需求,不需要把線程弄到等待隊列裏。
P():
標記減一,如果標記小於0了,說明資源不夠,需要把線程加入隊列;
V():
標記加一,如果標記小於等於0,說明等待隊列中有元素,喚醒一個隊列中的元素並執行;

信號量的用途:互斥和條件同步(注意等待的條件是獨立的互斥);
信號量的缺點:讀/開發代碼困難;容易出錯(使用的信號量被另一個線程佔用,完了釋放信號量);不能夠處理死鎖;

10.5 管程monitor

在這裏插入圖片描述
管程的目的:分離互斥和對條件同步的關注。
管程的定義:一個鎖(臨界區)加上0個或若干個條件變量(等待/通知信號量用於管理併發訪問的共享數據)。
管程實現的一般方法:收集在對象/模塊中的相關共享數據;定義方法來訪問共享數據。

一開始,所有進程在右上角的排隊隊列中,排隊完後進行wait()操作,等到signal()操作喚醒後,執行這個進程的代碼。

10.5.1 管程的組層

(1)Lock():
Lock::Acquire()如果鎖可用,就搶佔鎖(上鎖)
Lock::Release()釋放鎖,如果這個時候有等待者,就喚醒
Lock操作保證互斥。
(2)Contion Variable:
如果條件不能滿足就wait(),一旦條件滿足了,signal()會喚醒那些在隊列中的線程並繼續執行。

10.5.2 管程的實現

需要維持每個條件隊列;線程等待的條件是等待一個signal()操作。
在這裏插入圖片描述
(1)wait():
numWaiting就是計數器,統計有多少個線程處於等待隊列中;
release()因爲當前線程在睡眠,就必須把鎖打開一下,以便就緒狀態的線程去執行,如果不release可能會造成死鎖;
schedule()的意思是,當前線程在wait()了,在隊列裏睡眠了,此時就需要選擇一個就緒態的線程去執行;
就緒態的線程執行完畢後就可以再上鎖。
(2)signal():
如果等待隊列中有元素,那麼就把隊列的頭元素取出來(並刪掉)並喚醒wakeup,此時這個線程就是就緒狀態了,它的下一步操作就是wait()裏的schedule(),執行這個線程。
注意,signal僅在等待隊列中有元素的時候纔對numberWaiting執行(減法操作),而信號量不一樣,在P()和V()一定會有對信號量的加減操作的。
在這裏插入圖片描述

(1)Deposit():
(這裏比較高能,我也是看了好幾遍才明白的。)
根據管程定義,只有一個線程能進入管程,所以一開始就上鎖,在最後才解鎖(紫紅色字);這裏和信號量是不一樣的,信號量的互斥是緊緊靠着信號量的,而管程的互斥是在頭和尾。注意,這個lock是管程的lock。
如果buffer沒有滿,就可以先不看紅字,Deposit是要往buffer里加商品,加的時候需要上鎖保證對buffer操作的互斥(紫色,一次只運行一個線程操作);直到buffer的計數器爲n,裝滿了;
但是需要提前判斷一種情況,就是容器是不是滿的(紅字部分),如果是滿的,就把加入商品這個操作使用條件變量notFull,給notFull傳入的參數就是那個管程的lock。此時重點來了,還記得管程條件變量定義裏的wait()操作嗎?那裏面有一個release操作,釋放的就是紫紅色標記的lock->acquire();只有釋放了這個lock,現在處於就緒狀態(就是在notFull.signal()操作中被喚醒)的線程才能執行(就是wai()裏面的schedule()操作),否則就會死鎖;等到就緒狀態的線程執行完了,再上鎖,恢復原樣!
現在再看看notEmpty.release()是不是就好理解了?因爲我每進行一次deposit操作,buffer裏面就會多一個商品,那麼就多一個東西被消費者使用,此時就可以喚醒一個取商品操作的線程,這個就是靠notEmpty.release()實現,此時就有一個取商品線程處於就緒態,他會在remove()操作裏的notEmpty.Wait()操作中被執行!!
(2)Remove():
和上面就一樣啦,我就不解釋了。

10.5.3 兩個巨老的方法

在這裏插入圖片描述
上圖的黃色標記是等待的線程,藍色標記是正在執行的線程,它進行了喚醒操作。
注意,在上面的生產者消費者代碼執行過程中,一次執行signal操作可能會涉及到2個線程在同一個管程中的操作(是讓被喚醒的老黃色線程馬上schedule執行呢,還是先讓當前新藍色線程沒擦完的屁股擦擦呢?),哪一個先執行??關於這種情況的具體解決,有兩種方式:
Hoare:如上圖右圖。一旦執行了signal操作,先讓等待的黃色線程完事了,新藍色線程再把剩下的屁股擦了;
Hansen(採用):如上圖左圖。一旦執行了signal操作,新藍色線程先執行完,然後老黃色線程再執行。
在這裏插入圖片描述
根據兩個巨老的方案,修改deposit如上。Hansen實際上和之前一樣。之所以用while,是因爲上圖黃色線程必須等藍色的完成後纔會釋放,所以說在隊列的線程可能不是一個,都在搶線程;而Hoare方法,他是每signal一次,只有一個等待線程,於是用if。

10.5.4 本節總結

在這裏插入圖片描述
基本的硬件操作(禁用中斷/原子指令/原子操作)是高層抽象(信號量/鎖/條件變量)的基礎,採取不同的高層抽象的算法策略,可以實現臨界區和管程等不同的併發編程策略。

10.6 讀者寫者問題

目的:
->共享數據的訪問;

使用者類型:
->讀者(不需要修改數據);
->寫者(讀取和修改數據)。

問題的約束:
->允許同一時間有多個讀者,但是任何時間只能有一餓寫者;
->當沒有寫者時,讀者纔可以訪問數據;
->當沒有讀者和其他寫者時,寫者才能訪問數據;
->在任何時候只能有一個線程可以操作共享變量。

共享數據包括:
->數據集;
->信號量CountMutex(初始值爲1,約束讀者);
->信號量WriteMutex(初始值爲1,約束寫者);
->讀者數量Rcount(整數,初始值爲1)。

10.6.1 基於信號量的讀者優先

在這裏插入圖片描述
(1)寫者:
sem_wait(WriteMutex)相當於P()操作;
write;
sem_post(WriteMutex)相當於V()操作;這倆其實就是鎖。我們知道,二進制的P/V操作就是鎖。
確保一個時間只有一個寫者在揮灑筆墨。
(2)讀者:
在read的再之前,因爲要實現對Rcount的保護,所以首先給讀者上鎖;如果當前沒有讀者,此時給寫者上鎖(P()操作)不許寫者進來,然後我就可以讀了;因爲進來了一個讀者,所以Rcount++;給讀者解鎖;
在read的再之後,讀完了,因爲要實現對Rcount的保護,所以首先給讀者上鎖;讀者走一個,所以Rcount–;如果最後一個讀者也讀完了,把寫者的鎖釋放;給讀者解鎖。

10.6.2 基於管程的寫者優先

->基於讀者優先的方法,只要有一個讀者處於活動狀態,後來的讀者都會被接納。如果讀者源源不斷的出現,那麼寫者會飢餓。
->基於寫者優先的方法,一旦寫者就緒,那麼寫者會盡可能快的執行寫操作。如果寫者源源不斷的出現,那麼讀者就始終處於阻塞狀態。

注意,有兩類寫者,一種是正在創作的寫者,另一種是在等待隊列中的寫者。只要有一種寫者存在,讀者都要等待。
在這裏插入圖片描述
Database::Read():
要等待以上2種讀者;之後才能read;喚醒在等待隊列中的寫者;
Database::Write():
只需要等待正在讀或者正在寫的人;沒有人正在操作就可以write;喚醒其他人。
管程的數據:
AR/AW活着的讀者和寫者個數;WR/WW等待着的讀者和寫者的個數;
條件變量:okToRead可以去讀了;okToWrite可以去寫了。

(1)讀者的操作
在這裏插入圖片描述
StartRead():
(確保即沒有等待着寫者也沒有正在寫的寫者)
因爲是管程,所以兩端上鎖;
如果等待着寫者和正在寫的寫者數量和大於1,那麼等待的讀者加1,等着,注意,等完之後WR要減1,AR要加1。
DoneRead():
(讀完了,喚醒其他寫者)
因爲讀完了,AR減1;
如果沒有人在讀且有寫者在等着,馬上喚醒一個寫者。

(2)寫者的操作
在這裏插入圖片描述
StartWrite():
只要有人在操作共享變量,我就等着;等完了,我就變成active了。

DoneWrite():
寫完了,AW減1;先喚醒寫者,再喚醒讀者。

10.6 哲學家就餐問題

在這裏插入圖片描述
思路1,哲學家的角度看怎麼解決這個問題?
要麼不拿,要麼就拿兩個叉子。
在這裏插入圖片描述

思路2,計算機程序怎麼解決這個問題?
不能浪費CPU時間,而且線程之間要相互通信。
在這裏插入圖片描述

思路3,如何實現編程?
->必須有一個描述每個哲學家當前狀態的數據結構;
->該結構是一個臨界資源,各個哲學家對它的訪問應該互斥的進行(線程間互斥);
->一個哲學家喫飽後,需要喚醒左領右舍,所以存在同步關係(線程同步)。

注意,我們考慮的對象,也就是共享資源,是哲學家的狀態,而不是叉子的狀態。
在這裏插入圖片描述
一個哲學家所有操作的實現:
在這裏插入圖片描述

S2-S4拿叉子的實現:
注意,因爲涉及到對共享狀態量的訪問,所以需要上鎖。
在這裏插入圖片描述

其中,看看能不能拿兩把叉子的操作:
在這裏插入圖片描述
注意,這裏的V操作和之前的P操作對應上了。
在這裏插入圖片描述

S6-S7放下叉子的實現:
注意,因爲涉及到對共享狀態量的訪問,所以需要上鎖。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章