操作系統實驗——PV操作實現生產者消費者模型

操作系統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. 臨界區(互斥)問題,信號量初值需要置爲1,表示只能有一個進程進入臨界區,從而保護臨界區內的數據同時只能被一個進程訪問,避免出現多個進程同時操作同一個數據。

    Semaphore S;        //初始值爲1
    do{
        wait(S);
            Critical Section;		//臨界區
        signal(S);
            remainder section	//剩餘部分
    }while(1);
    
  2. 兩個進程的同步問題。假如有兩個進程 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操作解決同步問題)

  1. 先引入全局的信號量,將其封裝在一個類中

    class Global{
    	static syn empty = new syn(2);	//成員變量count表示剩餘空閒緩衝區的數量, >0則生產者進程可以執行
    	static syn full = new syn(0);	//成員變量count表示當前待消費物品的數量, >0則消費者進程可以執行
    	static int[] buffer = new int[2];	//緩衝區數組,大小代表緩衝區的數量,即放麪包的盤子
    }
    
  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的值表示緩衝
     		區的待消費物品的數量,相當於喚醒消費者*/
    		}
    	}
    }
    
  3. 消費者類

    /**
     * 單個消費者類
     * @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();	/*消費完一個物品後,釋放
     		一個緩衝區,給空閒緩衝區數量++,喚醒生產者可以生產
     		商品了*/
    		}
    	}
    }
    
  4. 主類測試

    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();
    	}
    }
    
  5. 總結

    運行結果

​ 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();
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章