1. 程式人生 > >PV操作和訊號量機制實現程序同步(對多個臨界資源的互斥訪問)

PV操作和訊號量機制實現程序同步(對多個臨界資源的互斥訪問)

          程序同步是我們在多執行緒中討論最多的一個話題,在大多數的開發語言中,他們都有自己實現程序同步的方法或者實現。但歸根結底他們實現的方式都是基於作業系統的程序同步的方式。今天我們就一起來看一下在作業系統這個底層中是怎麼實現程序同步的。在計算機作業系統中,PV操作是程序管理中的一個很重要的方式,這是重點也是難點。 在看PV操作之前,我們先來看另一個比較重要的概念——訊號量

          什麼是訊號量?訊號量(semaphore)的資料結構為一個值和一個指標,指標指向等待該訊號量的下一個程序。訊號量的值與相應資源的使用情況有關。當它的值大於0時,表示當前可用資源的數量;當它的值小於0時,其絕對值表示等待使用該資源的程序個數。注意,訊號量的值僅能由PV操作來改變。訊號量(Semaphore)

,有時被稱為訊號燈,是在多執行緒環境下使用的一種設施,是可以用來保證兩個或多個關鍵程式碼段不被併發呼叫。在進入一個關鍵程式碼段之前,執行緒必須獲取一個訊號量;一旦該關鍵程式碼段完成了,那麼該執行緒必須釋放訊號量。其它想進入該關鍵程式碼段的執行緒必須等待直到第一個執行緒釋放訊號量。為了完成這個過程,需要建立一個訊號量VI,然後將Acquire Semaphore VI以及Release Semaphore VI分別放置在每個關鍵程式碼段的首末端。確認這些訊號量VI引用的是初始建立的訊號量。 


      一般來說,訊號量S>=0時,S表示可用資源的數量。執行一次P操作意味著請求分配一個單位資源,因此S的值減1;當S<0時,表示已經沒有可用資源,請求者必須等待別的程序釋放該類資源,它才能執行下去。而執行一個V操作意味著釋放一個單位資源,因此S的值加1;若S<=0,表示有某些程序正在等待該資源,因此要喚醒一個等待狀態的程序,使之執行下去。

    利用訊號量和PV操作實現程序互斥的一般模型是:


程序P1              程序P2           ……          程序Pn
……                  ……                           ……
P(S);              P(S);                         P(S);
臨界區;             臨界區;                        臨界區;
V(S);              V(S);                        V(S);
……                  ……            ……           ……

    其中訊號量S用於互斥,初值為1。


    使用PV操作實現程序互斥時應該注意的是:
    (1)每個程式中使用者實現互斥的P、V操作必須成對出現,先做P操作,進臨界區,後做V操作,出臨界區。若有多個分支,要認真檢查其成對性。
    (2)P、V操作應分別緊靠臨界區的頭尾部,臨界區的程式碼應儘可能短,不能有死迴圈。
   (3)互斥訊號量的初值一般為1。

        PV操作是典型的同步機制之一。用一個訊號量與一個訊息聯絡起來,當訊號量的值為0時,表示期望的訊息尚未產生;當訊號量的值非0時,表示期望的訊息已經存在。用PV操作實現程序同步時,呼叫P操作測試訊息是否到達,呼叫V操作傳送訊息。  

對一個訊號量變數可以進行兩種原語操作:p操作和v操作,定義如下:

	Method p(var s:samephore);  
	{  
	s.value=s.value-1;  
	if (s.value>0) 
	asleep(s.next);  
	}  
	
	
	
	
	Method  v(var s:samephore);  
	{  
	s.value=s.value+1;  
	if (s.value<=0) wakeup(s.next);  
	}  


其中用到兩個標準過程:  

asleep(s.next);執行此操作的程序的PCB進入s.next尾部,程序變成等待狀態  

wakeup(s.queue);將s.queue頭程序喚醒插入就緒佇列  

s.value初值為1時,可以用來實現程序的互斥。 

p操作和v操作是不可中斷的程式段,稱為原語。如果將訊號量看作共享變數,則pv操作為其臨界區,多個程序不能同時執行,一般用硬體方法保證。一個訊號量只能置一次初值,以後只能對之進行p操作或v操作。由此也可以看到,訊號量機制必須有公共記憶體,不能用於分散式作業系統中,這是它最大的弱點。  

V原語的主要操作是:  

⑴value加1;  

若相加結果大於零,則程序繼續執行;  

若相加結果小於或等於零,則喚醒一阻塞在該訊號量上的程序,然後再返回原程序繼續執行或轉程序排程。

        在java JDK的併發包就給我提供了一個類似的訊號量類——Semaphore,其中的acquire()release()方法就相當於P和V操作,這兩個方法的特殊之處在於對Semaphore物件的操作都是原子操作,由作業系統底層來支援。下面我們就來看一下用java實現經典的哲學家就餐問題吧。

import java.util.concurrent.Semaphore;
class Signs{
final static int THINKING=0; //哲學家的狀態THINGING
final static int EATING=1; //哲學家的狀態EATING
static int[] status=new int[5]; //哲學家的狀態,預設都是THINGING
static Semaphore[] s=null; //訊號量:記錄哲學家是否可以進餐,不能進餐則堵塞
static Semaphore mutex=new Semaphore(1); //臨界區互斥訪問訊號量(二進位制訊號量),相當於互斥鎖
//初始化每個哲學家的進餐訊號量,預設值都不能進餐
static{
s=new Semaphore[5];
for(int i=0;i<s.length;i++)
s[i]=new Semaphore(0);
};
}
class Philosopher implements Runnable{
private int pid; //當前哲學家的序號
private int lid; //坐在左手邊的哲學家序號
private int rid; //坐在右手邊的哲學家序號
Philosopher(int id){
this.pid=id;
this.lid=(id+4)%5;
this.rid=(id+1)%5;
}
private void test(int pid){
//如果當前哲學家左右手邊的人都沒有吃飯,則當前哲學家可以進餐
if(Signs.status[pid]==Signs.THINKING&&Signs.status[lid]!=Signs.EATING&&Signs.status[rid]!=Signs.EATING){
Signs.status[pid]=Signs.EATING; //此時當前哲學家執行緒可以進餐,但其他哲學家執行緒很可能無法進餐
Signs.s[pid].release(); //釋放一個許可
}
}
public void run(){
try {
//嘗試拿起叉子準備進餐
Signs.mutex.acquire();
test(pid);
Signs.mutex.release();
//判斷當前哲學家的進餐訊號量,如果不能許可進餐,則當前執行緒阻塞
Signs.s[pid].acquire();
System.out.println("#"+pid+" 號哲學家正在進餐...");
//放下叉子,並喚醒旁邊兩個被阻塞進餐的哲學家,讓他們嘗試進餐
Signs.mutex.acquire();
Signs.status[pid]=Signs.THINKING;
test(lid); //讓左手邊的哲學家嘗試拿起叉子,如果可以,則釋放這個哲學家的訊號量許可
test(rid); //同上
Signs.mutex.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Test{
public static void main(String[] args) {
new Thread(new Philosopher(0)).start();
new Thread(new Philosopher(1)).start();
new Thread(new Philosopher(2)).start();
new Thread(new Philosopher(3)).start();
new Thread(new Philosopher(4)).start();
}
}


------------------------------------------------------------------------------------------------------------