1. 程式人生 > 實用技巧 >OS:6-管程與訊號量PV操作

OS:6-管程與訊號量PV操作

第6章 併發程式設計

6.1 併發程序

6.1.1 順序程式設計

  • 一個程序在處理器上的執行順序是嚴格按順序的
  • 不但指一個程式模組內部,也指兩個程式模組之間

特點:

  1. 程式執行的順序性
  2. 程式環境的封閉性
  3. 執行結果的確定性
  4. 計算過程的可再現性

6.1.2 程序的併發性

  • 程式的併發性指一組程序的執行在時間上是重疊的
    • 巨集觀上,一個時間段中幾個程序都在同一個處理器上
    • 微觀上,任一時刻僅有一個程序在處理器上執行
  • 併發程序的分類
    • 無關的:滿足Bernstein條件
    • 互動的:一個程序的執行可能影響其他併發程序的結果
  • Bernstein條件
    • R(pi)={ai1,ai2,ai3,..,ain},程序pi在執行期間引用的變數集
    • W(pi)={bi1,bi2,bi3,...,bin},程序pi在執行期間改變的變數集
    • (R(p1)∩W(p2)) (R(p1)∩W(p2)) (W(p1)∩W(p2)) = 空集
  • 互動的併發程序——與時間有關的錯誤
    • 結果不唯一
    • 永遠等待

6.1.3 程序的互動:競爭與協作

  • 競爭關係:兩個程序要訪問同一資源

    • 帶來兩個問題:死鎖和飢餓
    • 解決:互斥
      • 若干程序要使用同一共享資源時,任何時刻最多允許一個程序去使用
  • 協作關係:某些程序為了完成同一任務需要分工協作,合作的每一個程序不知道互相的進度,當合作程序中的一個到達協作點時,在尚未得到其夥伴程序發來的訊息或訊號之前應阻塞自己

    • 解決:同步
      • 一個程序的執行依賴於另一個協作程序的訊號,當一個程序沒有得到來自於另一個程序的訊息或訊號則需等待

6.2 臨界區管理

6.2.1 互斥與臨界區

  • 併發程序中
    • 共享變數有關的程式段——臨界區
    • 共享變數代表的資源——臨界資源
  • 臨界區排程原則
    • 一次至多一個程序進入臨界區
    • 如果已有程序在臨界區,其他試圖進入的程序需要等待
    • 進入臨界區的程序應該在有限時間內退出

6.2.2 Peterson演算法

  • 嘗試1:可能兩個程序都進去

  • 嘗試2:可能兩個程序都進不去

  • Peterson演算法

6.2.3 實現臨界區管理的硬體設施

  • 關中斷

  • 測試並建立指令

    bool TS(bool &x) {
    	if(x) {
    		x=false;
    		return true;
    	}else
    		return false;
    }
    
    //TS指令實現程序互斥
    bool s=true;
    cobegin
    process Pi( ) { //i=1,2,...,n
    	while(!TS(s)); //上鎖
    	{臨界區};
    	s=true; //開鎖
    }
    coend
    
  • 對換指令

    void SWAP(bool &a, bool &b) {
        bool temp=a;
        a=b;
        b=temp;
    }
    
    //對換指令實現程序互斥
    bool lock=false;
    cobegin
    Process Pi( ){ //i=1,2,...,n
    	bool keyi=true;
    	do {
    		SWAP(keyi,lock);
        }while(keyi); //上鎖
        {臨界區};
    	SWAP(keyi,lock); //開鎖
    }
    coend
    

6.3 訊號量與PV操作

6.3.1 訊號量與PV操作

struct semaphore
{ int count; QueueType queue; }
void P(semaphore s); // also named wait
{
	s.count - -;
	if (s.count < 0) 
    { place this process in s.queue; block this process }
};
void V(semaphore s); // also named signal
{
	s.count ++;
	if (s.count <= 0)
	{ remove a process from s.queue; convert it to ready state; }
};
  • count>0,代表可使用的資源數
  • count<0,絕對值表示等待程序數

6.3.2 經典互斥問題

飛機票問題

方案1:效能差

方案2:效能較好

哲學家就餐問題

  • 初步嘗試:存在死鎖!

一些解決方案:

  • 至多允許四個哲學家同時取叉子 (C. A. R. Hoare方案)
  • 奇數號先取左手邊的叉子,偶數號先取右手邊的叉子
  • 每個哲學家取到手邊的兩把叉子才吃,否則一把叉子也不取 (第五版教材, Page 188, AND型訊號量)
  • 解法1:至多允許四個哲學家同時取叉子

  • 解法2:奇數號先取左手邊的叉子,偶數號先取右手邊的叉子

6.3.3 經典同步問題

生產者-消費者問題

  • 一個生產者,一個消費者,一個緩衝單元

  • 一個生產者,一個消費者,多個緩衝單元

  • 多個生產者,多個消費者,多個緩衝單元

蘋果-桔子問題

6.3.4 其他習題

讀寫者問題

  • 讀者優先

  • 讀寫公平

    • 新增訊號量S,寫者程序有機會進佇列等,會阻斷兩波讀者,等前一波讀者讀完了就可以寫
  • 寫者優先

睡眠理髮師問題

理髮店理有一位理髮師、一把理髮椅和n把供等候理髮的顧客坐的椅子

如果沒有顧客,理髮師便在理髮椅上睡覺

一個顧客到來時,它必須叫醒理髮師

如果理髮師正在理髮時又有顧客來到,則如果有空椅子可坐,就坐下來等待,否則就離開

農夫獵人問題

有一個鐵籠子,每次只能放入一個動物。 獵手向籠中放入老虎,農夫向籠中放入羊;動物園等待取籠中的老虎,飯店等待取籠中的羊

其實就蘋果桔子問題

銀行業務問題

某大型銀行辦理人民幣儲蓄業務,由n個儲蓄員負責。每個顧客進入銀行後先至取號機取一個號,並且在等待區找到空沙發坐下等著叫號。 取號機給出的號碼依次遞增,並假定有足夠多的空沙發容納顧客。當一個儲蓄員空閒下來,就叫下一個號

其實就是生產消費問題,(n,n,inf)

注意到這裡多了個server_count,按照生產消費者的程式碼是不應該有的,為什麼呢?如果沒有的話,從customer角度,拍了號就結束了,P(server_count)代表了一個被叫號、服務的過程

緩衝區管理

有n個程序將字元逐個讀入到一個容量為80的緩衝區中(n>1),當緩衝區滿後,由輸出程序Q負責一次性取走這80個字元。這種過程迴圈往復,請用訊號量和P、V操作寫出n個讀入程序(P1, P2,…Pn)和輸出程序Q能正確工作的動作序列

本質上也是生產者消費者問題的變式

售票問題

汽車司機與售票員之間必須協同工作, 一方面只有售票員把車門關好了司機才能開車,因此,售票員 關好門應通知司機開車,然後售票員進行售票。另一方面,只有當汽車已經停下,售票員才能開門上下客,故司機停車後應該通知售票員。假定某輛公共汽車上有一名司機與兩名售票員,汽車當前正在始發站停車上客,試用訊號量與P、V操作寫出他們的同步演算法

吸菸者問題

一個經典同步問題:吸菸者問題(patil,1971)。三個吸菸者在一個房間內,還有一個香菸供應者。為了製造並抽掉香菸,每個吸菸者需要三樣東西:菸草、紙和火柴,供應者有豐富貨物提供。三個吸菸者中,第一個有自己的菸草, 第二個有自己的紙和第三個有自己的火柴。供應者隨機地將兩樣東西放在桌子上,允許一個吸菸者進行對健康不利的吸菸。當吸菸者完成吸菸後喚醒供應者,供應者再把兩樣東西放在桌子上,喚醒另一個吸菸者

這道題思路比較巧,用了一個逆向思維,訊號量代表的不是缺什麼,而是喚醒已經有什麼的人

6.4 管程

6.4.1 管程和條件變數

管程是由區域性於自己的若干公共變數及其說明和所有訪問這些公共變數的過程所組成的軟體模組

  • 為什麼要引入管程
    • 把分散在各程序中的臨界區集中起來進行管理
    • 防止程序有意或無意的違法同步操作
    • 便於用高階語言來書寫程式
  • 條件變數:是出現在管程內的一種資料結構,且只有在管程中才能被訪問,它對管程內的所有過程是全域性的,只能通過兩個原語操作來控制它
    • wait( )-阻塞呼叫程序並釋放管程,直到另一個程序在該條件變數上執行signal( )
    • signal( )-如果存在其他程序由於對條件變數執行wait( ) 被阻塞,便釋放之;如果沒有程序在等待,那麼,訊號不被儲存、
      • 使用signal釋放等待程序時,可能出現兩個程序同時停留在管程內
      • 霍爾(Hoare, 1974)採用:執行signal的程序等待,直到被釋放程序退出管程或等待另一個條件變數
type 管程名=monitor {
	區域性變數說明;
	條件變數說明;
	初始化語句;
	define 管程內定義的,管程外可呼叫的過程或函式名列表;
	use 管程外定義的,管程內將呼叫的過程或函式名列表;
	過程名/函式名(形式引數表) {
		<過程/函式體>;
	}
	…
	過程名/函式名(形式引數表) {
		<過程/函式體>;
	}
}

6.4.2 管程的實現

  • 霍爾方法使用P和V操作原語來實現對管程中過程的互斥呼叫,及實現對共享資源互斥使用的管理
  • 不要求signal操作是過程體的最後一個操作,且 wait和signal操作可被設計成可以中斷的過程

資料結構

  • mutex
    • 對每個管程,使用用於管程中過程互斥呼叫的訊號量 mutex (初值為1)
    • 程序呼叫管程中的任何過程時,應執行P(mutex);程序退出管程時,需要判斷是否有程序在next訊號量等待,如果有(即next_count>0),則通過V(next)喚醒一個發出signal的程序,否則應執行V(mutex)開放管程,以便讓其他呼叫者 進入
    • 為了使程序在等待資源期間,其他程序能進入管程,故在 wait操作中也必須執行V(mutex),否則會妨礙其他程序進入管程,導致無法釋放資源
  • nextnext-count
    • 對每個管程,引入訊號量next(初值為0),凡發出signal操作的程序應該用P(next)阻塞自己,直到被釋放程序退出管程或產生其他等待條件
    • 程序在退出管程的過程前,須檢查是否有別的程序在訊號量next上等待,若有,則用V(next)喚醒它。next-count(初值為0),用來記錄在next上等待的程序個數
  • x-semx-count
    • 引入訊號量x-sem(初值為0),申請資源得不到滿足時,執行P(x-sem)阻塞。由於釋放資源時,需要知道是否有別的程序在等待資源,用計數器x-count(初值為0)記錄等待資 源的程序數
    • 執行signal操作時,應讓等待資源的諸程序中的某個程序立即恢復執行,而不讓其他程序搶先進入管程,這可以用 V(x-sem)來實現

使用??-count的原因在於:訊號量規定,只允許使用P、V原語操作訪問訊號量,不能直接對訊號量的整型值做讀寫操作,也不能直接對訊號量的佇列做其他任何操作

管程操作

6.4.3 解決互斥問題

讀寫者問題

哲學家就餐問題

  • 其他解法

    TYPE dining = monitor{
    	semaphore f[5]}; // 叉子
    	semaphore room; // 房間
    	int f_count[5],room_count,rc=0;
    	bool f_use[5]={false};
    	InterfaceModule IM;
    	USE enter,leave,wait,signal;
    	DEFINE give,smoke;
    	procedure pickup(int i){ // i = 0,1,2,3,4
    		enter(IM);
    		if(rc==4) wait(room,room_count,IM);
    		rc++;
    		if(f_use[i]) wait(f[i],f_count[i],IM);
    		f_use[i]=true;
    		if(f_use[(i+1)%5]) wait(f[(i+1)%5],f_count[(i+1)%5],IM);
    		f_use[(i+1)%5]=true;
    		leave(IM);
    	}
    	procedure putdown(int i){ // i = 0,1,2,3,4
    		enter(IM);
    		f_use[i]=f_use[(i+1)%5]=false;
    		signal(f[i],f_count[i],IM);
    		signal(f[(i+1)%5],f_count[(i+1)%5],IM);
    		rc--;
    		signal(room,room_count,IM);
    		leave(IM);
    	}
    }
    cobegin
    	process philosopher_i(){ // i = 0,1,2,3,4
    		while(1){
    			pickup(i);
    			// 吃
    			putdown(i);
    		}
    	}
    coend
    

睡眠理髮師問題

理髮店理有一位理髮師、一把理髮椅和n把供等候理髮的顧客坐的椅子

如果沒有顧客,理髮師便在理髮椅上睡覺

一個顧客到來時,它必須叫醒理髮師

如果理髮師正在理髮時又有顧客來到,則如果有空椅子可坐,就坐下來等待,否則就離開

TYPE barbershop = monitor{
	semaphore barber,customer;
	int barber_count,customer_count;
	int chair=N,cc=0,bc=0;//cc=等待顧客,bc=可用理髮師
	InterfaceModule IM;
	USE enter,leave,wait,signal;
	DEFINE give,smoke;
	procedure barber(){
		enter(IM);
		if(cc==0) wait(customer,customer_count,IM); // 如果沒人來,睡覺
		cc--;
		leave(IM);
	}
	procedure barber_next(){
		enter(IM);
		signal(barber,barber_count,IM); // 送客,喚醒等待的客戶
		leave(IM);
	}
	procedure customer(){
		enter(IM);
		if(cc<chair){
			cc++;
			if(cc<=bc){
				signal(customer,customer_count,IM); // 客人來了,有睡覺的趕緊給爺起
			}else{
				wait(barber,barber_count,IM); // 在椅子上等
			}
		}// 沒椅子,溜了
		leave(IM);
	}
}
cobegin
	process barber_i(){ // i = 0,1,2,3,..
		barbershop.bc++;
		while(1){
			barbershop.barber();
			// 理髮
			barbershop.barber_next();
		}
	}
	process customer_i(){ // i = 0,1,2,3,...
		while(1){
			barbershop.customer();
			// 理髮
		}
	}	
coend

生產消費者的一個變式

6.4.4 解決同步問題

生產者-消費者問題

蘋果桔子問題

6.4.5 其他經典問題

吸菸者問題

TYPE smoke = monitor{
	semaphore s[3]={0,0,0};
	semaphore g=1;
	int s_count[3]={0,0,0},g_count;
	bool sc[3]={false};
	InterfaceModule IM;
	USE enter,leave,wait,signal;
	DEFINE give,smoke;
	procedure give(){
		enter(IM);
		wait(g,g_count,IM);
		// 隨機取i,j
		if((i==0&&j==1)||(j==0&&i==1)){
			signal(s[2],s_count[2],IM);
		}else if((i==0&&j==2)||(j==0&&i==2)){
			signal(s[1],s_count[1],IM);
		}else if((i==2&&j==1)||(j==2&&i==1)){
			signal(s[0],s_count[0],IM);
		}
		leave(IM);
	}
	procedure smoke(int i){
		enter(IM);
		if(!sc[i]) 
			wait(s[i],s_count[i],IM);
		sc[i] = false;
		// 抽菸
		signal(g,g_count,IM);
		leave(IM);
	}
}
cobegin
	process giver(){
		while(1)
			give();
	}
	process smoker_i(){ // i = 0,1,2
		while(1)
			smoke(i);
	}
coend

生產線裝配問題

設兒童小汔車生產線上有一隻大的儲存櫃,其中有N 個槽(N為5的倍數且其值≥5),每個槽可存放1個 車架或1個車輪;設有3組生產工人,其活動如下,試用管程實現這三組工人的生產合作工作

將N個槽口分為兩部分:N/5和4N/5,分別裝車架和車輪

車架 box1[N/5];

車輪 box2[4*N/5] ;

TYPE pipeline = monitor{
	semaphore S1,S2,S3,S4;
	int S1_count,S2_count,S3_count,S4_count;
	int c1,c2;
	InterfaceModule IM;
	USE enter,leave,wait,signal;
	DEFINE put1,put2,take;
	procedure put1(){
		enter(IM);
		if(c1==N/5)	wait(S1,S1_count,IM);
		// 生產車架並放入
		c1++;
		signal(S3,S3_count,IM);
		leave(IM);
	}
	procedure put2(){
		enter(IM);
		if(c2==4N/5)	wait(S2,S2_count,IM);
		// 生產車輪並放入
		c2++;
		if(c2%4==0)	signal(S4,S4_count,IM);
		leave(IM);
	}
	procedure take(){
		enter(IM);
		if(c1==0)	wait(S3,S3_count,IM);
		// 取一個車架
		c1--;
		if(c2<4)	wait(S4,S4_count,IM);	
		// 取四個車輪
		c2-=4;
		signal(S1, S1_count, IM);
		signal(S2, S2_count, IM);
		leave(IM);
	}	
}
cobegin
	略
coend

6.5 程序通訊

訊息傳遞提供了這些功能,最典型的訊息傳遞原語

  • send 傳送訊息的原語

  • receive 接收訊息的原語

6.5.1 直接通訊

  • 對稱直接定址,傳送程序和接收程序必須命名對方以便通訊,原語send() 和 receive()定義如下:
    • send(P, messsage) 傳送訊息到程序P
    • receive(Q, message) 接收來自程序Q的訊息
  • 非對稱直接定址,只要傳送者命名接收者,而接收者不需要命名傳送者,send()和 receive()定義如下:
    • send(P, messsage) 傳送訊息到程序P
    • receive(id, message) 接收來自任何程序的訊息,變數 id置成與其通訊的程序名稱
  • 訊息格式

6.5.2 間接通訊

訊息不是直接從傳送者傳送到接收者,而是傳送到由臨時 儲存這些訊息的佇列組成的一個共享資料結構,這些佇列通常成為信箱(mailbox)

一個程序給合適的信箱傳送訊息,另一程序從信箱中獲得訊息

間接通訊的send()和receive()定義如下:

  • send(A,message):把一封信件(訊息)傳送到信箱A

    • 如果指定的信箱未滿,則將信件送入信箱中由指標所指示的位置,並釋放等待該信箱中信件的等待者;否則,傳送信件者被置成等待信箱狀態
  • receive(A,message):從信箱A接收一封信件(訊息)

    • 接收信件:如果指定信箱中有信,則取出一封信件,並釋放等待信箱的等待者,否則,接收信件者被置成等待信箱中信件的 狀態

(注:R為出,W為入)

  • 應用:求解生產者消費者問題

6.5.3 訊息緩衝通訊

注意在複製過程中系統會將接收程序名換成傳送程序名,以便接收者識別

6.6 死鎖

6.6.1 死鎖產生

  • 程序推進順序不當

  • PV操作不當

  • 資源分配不當

    若系統中有m個資源被n個程序共享,每個程序都要求K個資源,而m < n·K時, 即資源數小於程序所要求的總數時,如果分配不得當就可能引起死鎖(這句話表述不太對)

    當 m≤n 時,每個程序最多請求 1 個這類資源時,系統一定不會發生死鎖。當 m>n 時, 如果 m/n 不整除,每個程序最多可以請求”商+1”個這類資源,否則為”商”個資源,使系統一定不會發生死鎖

    可以這麼理解,只要有一個程序可以滿足了,那麼它就能執行完成並釋放它佔有的資源,其他程序也可以接著被滿足

  • 對臨時性資源使用不加限制

    程序P1等待程序P3的信件S3來到後再向程序P2傳送信件S1;P2又要等待P1的信件S1來到後再向P3傳送信件S2;而P3也要等待P2的信件S2來到後才能發出信件S3。這種情況下形成了迴圈等待,產生死鎖

6.6.2 死鎖防止

系統形成死鎖的四個必要條件

  • 互斥條件(mutual exclusion):系統中存在臨界資源,程序應互斥地使用這些資源
  • 佔有和等待條件(hold and wait):程序請求資源得不到滿足而等程序請求資源得不到滿足而等待時,不釋放已佔有的資源
  • 不剝奪條件(no preemption):已被佔用的資源只能由屬主釋放,不允許被其它程序剝奪
  • 迴圈等待條件(circular wait):存在迴圈等待鏈,其中,每個程序都在鏈中等待下一個程序所持有的資源,造成這組程序永遠等待

死鎖防止的一些方法:

  • 破壞第一個條件:
    • 使資源可同時訪問而不是互斥使用
    • 但是有的資源不允許
  • 破壞第二個條件:
    • 靜態分配,程序在執行中不再申請資源,就不會出現佔有某些資源再等待另一些資源的情況
    • 降低資源利用率
  • 破壞第三個條件:
    • 採用剝奪式排程方法
    • 當程序在申請資源未獲准許的情況下,如主動釋放資源(一種剝奪式),然後才去等待。
  • 破壞第四個條件

層次分配策略(破壞條件2、4)

資源被分成多個層次

  • 當程序得到某一層的一個資源後,它只能再申請較高層次的資源

  • 當程序要釋放某層的一個資源時,必須先釋放佔有的較高層次的資源

  • 當程序得到某一層的一個資源後,它想申請該層的另一個資源時,必須先釋放該層中的已佔資源

例如,將資源排序,r1,r2……,rm ,規定如果程序不得在佔用資源ri(1≤i≤m)後再申請 rj(j<i)

6.6.3 死鎖避免

銀行家演算法

注意一個解題格式

6.6.4 死鎖檢測和解除

本質上還是歸結於銀行家演算法