一、PV操作
PV操作是一種實現進程互斥與同步的有效方法。PV操作與信號量的處理相關。P(passeren)通過,理解為申請資源,V(vrijgeven)釋放,理解為釋放資源。
PV操作是典型的同步機制之一。用一個信號量與一個消息聯系起來,當信號量的值為0時,表示期望的消息尚未產生;當信號量的值非0時,表示期望的消息已經存在。用PV操作實現進程同步時,調用P操作測試消息是否到達,調用V操作發送消息。
PV操作,原來這是狄克斯特拉用荷蘭文定義的,因為在荷蘭文中,通過叫passeren,釋放叫vrijgeven,PV操作因此得名。這是在計算機術語中不是用英語表達的極少數的例子之一。
二、信號量
信號量(semaphore)的概念和PV操作是荷蘭科學家E.W.Dijkstra提出來的。信號是鐵路交通管理中的一種常用設備,交通管理人員利用信號顏色的變化來實現交通管理。在操作系統中,信號量S是一整數。S大於或等於零,代表可供並發進程使用的資源實體數;在S小於零時,ISl表示正在等待使用資源實體的進程數。建立一個信號量必須說明此信號量所代表的意義並且賦初值。除賦初值外,信號量僅能通過PV操作來訪問。
信號量按其用途可分為兩種:
①公用信號量。聯系一組並發進程,相關的進程均可在此信號量上執行P操作和V操作,初值常常為1,用於實現進程互斥。
②私有信號量。聯系一組並發進程,僅允許擁有此信號量的進程執行P操作,而其他相關進程可在其上施行V操作。初值常常為0或正整數,多用於實現進程同步。
PV操作是由兩個操作,即P操作和V操作組成的。P操作和V操作是兩個在信號量上進行操作的過程,假定用S表示信號量,則把這兩個過程記作P(S)和V(S)。
三、PV操作原理
用PV操作來管理共享資源時,首先要確保PV操作自身執行的正確性。由於P(S)和V(S)都是在同一個信號量S上操作,為了使得它們在執行時不發生因交叉訪問信號量S而可能出現的錯誤,約定P(S)和V(S)必須是兩個不可被中斷的過程,即讓它們在屏蔽中斷下執行。把不可被中斷的過程稱為原語。於是,P操作和V操作實際上應該是P操作原語和V操作原語。
P操作的主要動作是:
①S減1;
②若S減1后仍大於或等於0,則進程繼續執行;
③若S減1后小於0,則該進程被阻塞后放入等待該信號量的等待隊列中,然后轉進程調度。
V操作的主要動作是:
①S加1;
②若相加后結果大於0,則進程繼續執行;
③若相加后結果小於或等於0,則從該信號的等待隊列中釋放一個等待進程,然后再返回原進程繼續執行或轉進程調度。
PV操作對於每一個進程來說,都只能進行一次,而且必須成對使用。在PV原語執行期間不允許有中斷發生。原語不能被中斷執行,因為原語對變量的操作過程如果被打斷,可能會去運行另一個對同一變量的操作過程,從而出現臨界段問題。如果能夠找到一種解決臨界段問題的元方法,就可以實現對共享變量操作的原子性。
四、進程的同步與互斥
1,進程同步
(1)調用P操作測試消息是否到達
任何進程調用P操作可測試到自己所期望的消息是否已經到達。若消息尚未產生,則S=0,調用P(s)后,P(S)一定讓調用者成為等待信號量S的狀態,即調用者此時必定等待直到消息到達;若消息已經存在,則S≠0,調用P(S)后,進程不會成為等待狀態而可繼續執行,即進程測試到自己期望的消息已經存在。
(2)調用V操作發送消息
任何進程要向其他進程發送消息時可調用V操作。若調用V操作之前S=0,表示消息尚未產生且無等待消息的進程,則調用V(S)后,V(s)執行S:=S+1使S≠0,即意味着消息已存在;若調用V操作之前S<0,表示消息未產生前已有進程在等待消息,則調用V(S)后將釋放一個等待消息者,即表示該進程等待的消息已經到達,可以繼續執行。
在用PV操作實現同步時,一定要根據具體的問題來定義信號量和調用P操作或V操作。一個信號量與一個消息聯系在一起,當有多個消息時必須定義多個信號量;測試不同的消息是否到達或發送不同的消息時,應對不同的信號量調用P操作或V操作。
2,進程互斥
(1)設立一個互斥信號量S,表示臨界區,其取值為1,0,-1,…其中,S=1表示無並發進程進入S臨界區;S=0表示已有一個並發進程進入了S臨界區;S等於負數表示已有一個並發進程進入S臨界區,且有|S|個進程等待進入S臨界區,S的初值為1。
(2)用PV操作表示對S臨界區的申請和釋放。在進入臨界區之前,通過P操作進行申請,在退出臨界區之后,通過V操作釋放。
五、相關推論
推論1:若信號量S為正值,則該值等於在阻塞進程之前對信號量S可施行的P操作數,亦即等於S所代表的實際還可以使用的物理資源數。
推論2:若信號量s為負值,則其絕對值等於登記排列在該信號量S等待隊列之中的進程個數,亦即恰好等於對信號量S實施P操作而被阻塞並進入信號量S等待隊列的進程數。
推論3:通常,P操作意味着請求一個資源,V操作意味着釋放一個資源。在一定條件下,P操作代表阻塞進程操作,而V操作代表喚醒被阻塞進程的操作。
六、生產者消費者問題
P操作定義如下:
1.mutex減1。
2.若mutex>=0,則P操作返回,該線程可以”通過“並繼續執行。
3.若mutex<0,則該線程被阻塞,進入操作系統的阻塞隊列。
V操作定義如下:
1.mutex加1。
2.若mutex>0,則V操作返回,該線程繼續執行。
3.若mutex<=0,則從阻塞隊列中喚醒一個阻塞在該信號量上的線程,然后再返回原線程(調用V操作的線程)繼續執行。
核心思想:
1、該類問題需要注意對緩沖區大小為n的處理,當緩沖區中有空時,便可對 empty 變量執行 P 操作,一旦取走一個資源便可執行 V 操作以釋放空閑區。對 empty 和 full 變量的 P 操作 必須放在 mutex 的P操作之前。
2、P操作即wait操作表示進程請求一個資源,V操作即signal表示進程釋放一個資源,且信號量機制遵循了同步機制的“讓權等待”原則。
3、wait():當緩沖區已滿/空時,生產者或消費者線程停止自己的執行,釋放鎖,使自己處於等待狀態,讓其它線程執行。notify():當生產者或消費者向緩沖區放入或取出一個產品時,向其他等待的線程發出通知,同時釋放鎖,使自己處於等待狀態,讓其它線程執行。wait()、nofity()這兩個方法必須有鎖對象調用,而任意對象都可以作為 synchronized 的同步鎖。
4、使用synchronized修飾的方法,叫做同步方法,保證A線程執行該方法的時,其他線程只能在方法外等待。被final修飾的方法是一個最終方法,不能被重寫,重寫會報錯。
1 import java.util.Scanner; 2
3 public class PV { 4
5 //信號量
6 static class Semaphore { 7 public int value; 8 public Semaphore(int value) { 9 this.value = value; 10 } 11 //P操作(passeren,通過)
12 public synchronized final void P() { 13 value--; 14 if (value < 0) { 15 try { 16 //當緩沖區已滿/空時,生產者/消費者線程停止自己的執行,釋放鎖,使自己處於等待狀態,讓其它線程執行
17 this.wait(); 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 } 22 } 23 //V操作(vrijgeven,釋放)
24 public synchronized final void V() { 25 value++; 26 if (value <= 0) { 27 //當生產者或消費者向緩沖區放入或取出一個產品時,向其他等待的線程發出通知,同時釋放鎖,使自己處於等待狀態,讓其它線程執行。
28 this.notify(); 29 } 30 } 31 } 32
33 static class Global { 34 //空閑緩沖區初始化為3
35 public static Semaphore empty = new Semaphore(3); 36 //滿緩沖區初始化為空
37 public static Semaphore full = new Semaphore(0); 38 //臨界區互斥信號量
39 public static Semaphore mutex = new Semaphore(1); 40 //count用於緩沖區中的進程進行計數
41 public static int count = 0; 42 //定時等待
43 public static void timingwait() { 44 try { 45 Thread.sleep(2000); 46 } catch (InterruptedException e) { 47 e.printStackTrace(); 48 } 49 } 50 } 51
52 //生產者
53 class Producer implements Runnable { 54 @Override 55 public void run() { 56 String threadName = Thread.currentThread().getName(); 57 Global.timingwait(); 58 System.out.println(threadName + " 生產出一個商品..."); 59 //獲取空緩沖區單元
60 Global.empty.P(); 61 //進入臨界區
62 Global.mutex.P(); 63 Global.timingwait(); 64 System.out.println(threadName + " 將產品放入緩沖區--緩沖區剩余 " + (++Global.count) + " 個產品"); 65 //離開臨界區,釋放信號量
66 Global.mutex.V(); 67 //滿緩沖區數加1
68 Global.full.V(); 69 } 70 } 71
72 //消費者
73 class Consumer implements Runnable { 74 @Override 75 public void run() { 76 String threadName = Thread.currentThread().getName(); 77 Global.timingwait(); 78 //獲取滿緩沖區單元
79 Global.full.P(); 80 //進入臨界區
81 Global.mutex.P(); 82 Global.timingwait(); 83 System.out.println(threadName + " 從緩沖區取出一個產品--緩沖區剩余 " + (--Global.count) + " 個產品"); 84 //離開臨界區,釋放互斥信號量
85 Global.mutex.V(); 86 //空緩沖區加1
87 Global.empty.V(); 88 System.out.println(threadName + " 消費一個商品..."); 89 } 90 } 91
92 public static void main(String[] args) { 93 int producer, consumer; 94 Scanner sc = new Scanner(System.in); 95 System.out.print("請輸入生產者數目:"); 96 producer = sc.nextInt(); 97 System.out.print("請輸入消費者數目:"); 98 consumer = sc.nextInt(); 99 for (int i = 0; i < producer; i++) { 100 new Thread(new PV().new Producer(), "生產者" + i + "號").start(); 101 } 102 for (int i = 0; i < consumer; i++) { 103 new Thread(new PV().new Consumer(), "消費者" + i + "號").start(); 104 } 105 } 106 }
執行結果: