操作系統PV操作之——生產者消費者模型
個人博客主頁
參考資料:
Java實現PV操作 | 生產者與消費者
浙大公開課
在操作系統的多進程、多線程操作中經常會有因爲同步、互斥等等問題引發出的一系列問題,我們的前輩爲了解決這些問題,發明出了“信號量(Semaphore)”這麼一個令人稱奇的變量,就目前來看,很巧妙的解決了這些問題。
- 信號量是個整形變量
- 信號量S只允許兩個標準操作wait()和signal(),或者他的發明者稱呼的P操作和V操作
- wait()和signal()是原子操作,不可分割的原語
對PV操作的定義
對PV操作的定義不是單一的,這裏舉個比較簡單的例子
/*P操作*/
wait(S){
value--;
if(value < 0){
/*value的大小表示了允許同時進入臨界區進行操作的
進程數量*/
/*add this process to waiting queue*/
block();
}
}
/*V操作*/
signal(S){
value++;
if(value <= 0){
/*因爲P操作是當value<0時休眠一個線程,說明如果有休眠
的線程,則value一定小於0,所以此時當value+1後,如果
還有休眠的線程,value必定小於或等於0 */
/*remove a process P from the waiting queue*/
wakeup(P);
}
}
信號量的應用
-
臨界區(互斥)問題,信號量初值需要置爲1,表示只能有一個進程進入臨界區,從而保護臨界區內的數據同時只能被一個進程訪問,避免出現多個進程同時操作同一個數據。
Semaphore S; //初始值爲1 do{ wait(S); Critical Section; //臨界區 signal(S); remainder section //剩餘部分 }while(1);
-
兩個進程的同步問題。假如有兩個進程 Pi和Pj,Pi有個A語句(輸入x的值),Pj有個B語句(輸出x+1的值),希望在B語句執行之前A語句已經執行完成。
//同步,定義信號量flag初值爲0,等待方用wait操作,被等待方用signal操作,還要緊貼着放 Pi進程 Pj進程 ... ... A wait(flag) signal(flag) B ... ...
生產者消費者模型
先定義出PV操作的類
/**
* 封裝的PV操作類,爲了簡單起見,沒有用一個等待隊列,而是直接用
* Java的Object類方法中的wait方法來模擬
* @author Vfdxvffd
* @count 信號量
* 這裏調用wait方法和signal方法的是同一個對象this,所以V操作喚
* 醒的只能是同一個對象P操作加入等待隊列的進程
*/
class syn{
int count = 0;
syn(){}
syn(int a){count = a;} //給信號量賦初值
public synchronized void Wait() {
count--;
if(count < 0) { //block
/*add this process to waiting queue*/
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void Signal() {
count++;
if(count <= 0) { //wakeup
/*remove a process P from the waiting queue*/
notify();
}
}
}
單生產單消費(PV操作解決同步問題)
-
先引入全局的信號量,將其封裝在一個類中
class Global{ static syn empty = new syn(2); //成員變量count表示剩餘空閒緩衝區的數量, >0則生產者進程可以執行 static syn full = new syn(0); //成員變量count表示當前待消費物品的數量, >0則消費者進程可以執行 static int[] buffer = new int[2]; //緩衝區數組,大小代表緩衝區的數量,即放麪包的盤子 }
-
生產者類
/** * 單個生產者類 * @author Vfdxvffd * @count 生產的物品數量標號 */ class Producer implements Runnable{ int count = 0; //數量 @Override public void run() { while(count < 20) { //最多生產20件商品 Global.empty.Wait(); /*要生產物品了,給剩餘空 閒緩衝區數量--,如果減完後變爲負數,則說明當前沒 有空閒緩衝區,則加入等待隊列*/ //臨界區,生產商品 int index = count % 2; Global.buffer[index] = count; System.out.println("生產者在緩衝區"+index+"中生產了物品"+count); count++; /*可以在此處讓進程休眠幾秒鐘,然後就可能在此時 CPU將資源調給消費者,但是可以發現由於下面語句還 未執行,所以消費者拿到CPU執行權也只能消費掉前幾 次生產的商品,這次生產的商品依舊無法被消費*/ Global.full.Signal();/*發出一個信號,表示緩衝區已 經有物品了,可以來消費了,成員變量count的值表示緩衝 區的待消費物品的數量,相當於喚醒消費者*/ } } }
-
消費者類
/** * 單個消費者類 * @author Vfdxvffd * @count 物品數量標號 */ class Consumer implements Runnable{ int count = 0; @Override public void run() { while(count < 20) { Global.full.Wait(); /*要消費物品了,給當前待消費 物品--,如果減完爲負數,則說明當前沒有可消費物品, 加入等待隊列*/ //臨界區 int index = count % 2; int value = Global.buffer[index]; System.out.println("消費者在緩衝區"+index+"中消費了物品"+value); count++; /*可以在此處讓進程休眠幾秒鐘,然後就可能在此時CPU將 資源調給生產者,但是可以發現由於下面語句還未執行, 所以生產者拿到CPU執行權也只能生產在前幾次消費的商品 騰出的緩衝區,這次消費的商品騰出的地方依舊無法被用 於生產*/ Global.empty.Signal(); /*消費完一個物品後,釋放 一個緩衝區,給空閒緩衝區數量++,喚醒生產者可以生產 商品了*/ } } }
-
主類測試
public class ConsumeAndProduce{ public static void main(String[] args) { Producer pro = new Producer(); Consumer con = new Consumer(); Thread t1 = new Thread(pro); Thread t2 = new Thread(con); t1.start(); t2.start(); } }
-
總結
empty和full兩個信號量的作用就在於消費者消費之前檢查是否有待消費商品,如果有則讓他去消費,沒有就要將消費者進程放進等待隊列,等到生產者生產了商品後又將其從等待隊列中取出。生產者在生產之前需要先檢查是否有足夠的緩衝區(存放商品的地方),如果有則讓其去生產,沒有的話就要進入等待隊列等待消費者消費緩衝區的商品。
多生產者多消費者(PV操作解決互斥問題)
因爲有多個生產者和消費者來生產和消費商品,我們需要在Global中加入兩個變量pCount、cCount,分別用來表示生產的商品的序號和被消費的商品的序號,之前是因爲只有單個生產消費着,所以直接將其定義在run方法中即可,但這是有多個生產者和消費者,所以要放到一個公共區,一起去操作它。但是如果我們有多個生產者和多個消費者,會不會出現線程安全問題?答案是肯定的。生產者重複生產了同一商品
這種情況如何出現的呢,我們先看run方法裏的代碼
@Override
public void run() {
while(Global.pCount < 20) { //最多生產20件商品
Global.empty.Wait();
//臨界區
int index = Global.pCount % 2;
Global.buffer[index] = Global.pCount;
System.out.println(Thread.currentThread().getName()+"生產者在緩衝區"+index+"中生產了物品"+Global.pCount);
Global.pCount++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Global.full.Signal();
}
}
假如生產者1號生產了0號商品,但此時他還沒做Global.pCount++
這一步操作,CPU將執行權切換到生產者2號,這時Global.pCount的值還是剛剛的0,沒有加1,所以又會生產出一個0號商品,那消費者也同理,消費完還沒加1,就被切換了執行權。
那就有個問題,如果我們將Global.pCount++
這一步提前能不能解決問題呢,當然也是不行的,因爲可能++完還沒輸出就被切換執行權,那下次執行權回來時候就會繼續執行輸出操作,但此時的Global.pCount的值已經不知道加了多少了。
解決方法
解決的辦法就是加入新的信號量Mutex,將初始值設爲1,引起多個生產者之間的互斥,或者多個消費者之間的互斥,即1號生產者操作pCount這個數據的時候,其他生產者無法對pCount進行操作。就是我們說的信號量的第一個應用,解決互斥問題。
package OS;
/**
* 封裝的PV操作類
* @author Vfdxvffd
* @count 信號量
*/
class syn{
int count = 0;
syn(){}
syn(int a){count = a;}
public synchronized void Wait() {
count--;
if(count < 0) { //block
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void Signal() {
count++;
if(count <= 0) { //wakeup
notify();
}
}
}
class Global{
static syn empty = new syn(2); //成員變量count表示剩餘空閒緩衝區的數量 >0則生產者進程可以執行
static syn full = new syn(0); //成員變量count表示當前待消費物品的數量 >0則消費者進程可以執行
static syn pMutex = new syn(1); //保證生產者之間互斥的信號量
static syn cMutex = new syn(1); //保證消費者之間互斥的信號量
static int[] buffer = new int[2];//緩衝區,就像放麪包的盤子
static int pCount = 0; //生產者生產的商品編號
static int cCount = 0; //消費者消費的商品編號
}
/**
* 生產者類
* @author Vfdxvffd
* @count 生產的物品數量標號
* Global.empty.Wait();和Global.pMutex.Wait();的順序無所謂,
* 只要和下面對應即可,要麼都包裹在裏面,要麼都露在外面
*/
class Producer implements Runnable{
@Override
public void run() {
while(Global.pCount < 20) { //最多生產20件商品
Global.empty.Wait(); /*要生產物品了,給剩餘空
閒緩衝區數量--,如果減完後變爲負數,則說明當前沒有
空閒緩衝區,則加入等待隊列*/
Global.pMutex.Wait(); /*保證生產者之間的互斥,
就像是加了一個鎖一樣this.lock()*/
//臨界區
int index = Global.pCount % 2;
Global.buffer[index] = Global.pCount;
System.out.println(Thread.currentThread().getName()+"生產者在緩衝區"+index+"中生產了物品"+Global.pCount);
Global.pCount++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Global.pMutex.Signal();//相當於釋放鎖this.unlock()
Global.full.Signal();/*發出一個信號,表示緩衝區已
經有物品了,可以來消費了,成員變量count的值表示緩衝
區的待消費物品的數量,相當於喚醒消費者*/
}
}
}
/**
* 消費者類
* @author Vfdxvffd
* @count 物品數量標號
* Global.full.Wait();和Global.cMutex.Wait();的順序無所謂,
* 只要和下面對應即可,要麼都包裹在裏面,要麼都露在外面
*/
class Consumer implements Runnable{
@Override
public void run() {
while(Global.cCount < 20) {
Global.full.Wait(); /*要消費物品了,給當前待消費
物品--,如果減完爲負數,則說明當前沒有可消費物品,
加入等待隊列*/
Global.cMutex.Wait();//保證消費者之間的互斥
//臨界區
int index = Global.cCount % 2;
int value = Global.buffer[index];
System.out.println(Thread.currentThread().getName()+"消費者在緩衝區"+index+"中消費了物品"+value);
Global.cCount++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Global.cMutex.Signal();
Global.empty.Signal(); /*消費完一個物品後,釋放
一個緩衝區,給空閒緩衝區數量++*/
}
}
}
public class ConsumeAndProduce{
public static void main(String[] args) {
Producer pro = new Producer();
Consumer con = new Consumer();
Thread t1 = new Thread(pro);
Thread t2 = new Thread(con);
Thread t3 = new Thread(pro);
Thread t4 = new Thread(con);
t1.start();
t2.start();
t3.start();
t4.start();
}
}