作業系統實驗——讀者寫者模型(寫優先)

頭髮是我最後的倔強發表於2020-09-13

作業系統實驗——讀者寫者模型(寫優先)

個人部落格主頁
參考資料:
Java實現PV操作 | 生產者與消費者

讀者寫者

對一個公共資料進行寫入和讀取操作,和之前的生產者消費者模型很類似,我們梳理一下兩者的區別。

  • 都是多個執行緒對同一塊資料進行操作
  • 生產者與生產者之間互斥、消費者與消費者之間互斥、生產者與消費者之間互斥
  • 寫者與寫者之間互斥、讀者與寫者之間互斥、但讀者與讀者之間併發進行

寫優先是說當有讀者進行讀操作時,此時有寫者申請寫操作,只有等到所有正在讀的程式結束後立即開始寫程式

定義PV操作

/**
 * 封裝的PV操作類
 * @count 訊號量
 */
class syn{        
    int count = 0;
    
    syn(){}
    syn(int a){count = a;}
	//P操作
    public synchronized void Wait() {
        count--;
        if(count < 0) {        //block
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
	//V操作
    public synchronized void Signal() {
        count++;
        if(count <= 0) {    //wakeup
        	notify();
        }
    }
}

全域性訊號量

全域性訊號量中用到了三個訊號量w、rw、mutex,初始化都等於1。下面一一做解釋。

  • 先從最簡單的mutex說,mutex用來互斥訪問count變數,對讀者數目的加加減減。
  • 然後是rw,當第一個讀程式進行讀操作時候,會持有rw鎖而不釋放,在它讀的過程中如果有寫程式想要寫資料,就無法在此時進行寫操作,此時可能還會進來多個讀程式,而只有當最後一個讀程式執行完讀操作的時候才會將rw鎖釋放。從而保證瞭如果在有一個或多個讀者正在進行讀操作時,寫程式試圖寫資料,只能等到所有正在讀的程式讀完才行。
  • 最後是w鎖,也是最複雜的一個,作用有二:
    • 保證了寫者與寫者之間的互斥,這個是很簡單的
    • 保證了寫優先的操作,是必要而不充分條件。如果此時有三個讀程式正在進行讀操作,而此時有一個寫程式進入試圖進行寫操作,由於第一個讀者進入時持有了rw鎖,而導致寫者在持有w鎖後(讀者程式雖然剛開始也會持有w鎖,但都是很快又釋放的,所以不影響寫程式獲取w鎖資源)被wait在rw鎖那塊,其實執行的wait方法是rw.wait(),而它本身還是持有w鎖的,也就是說之後如果還有讀/寫程式試圖進行讀操作時,就會在剛開始因為無法獲取w鎖資源而被wait,執行的wait語句是w.wait(),因為w鎖被寫程式持有,所以在寫程式寫完之前都不會釋放,當最後一個讀者讀完後,執行notify方法,其實是對rw鎖的釋放rw.notify(),此時也只有那個等待的寫者程式可以被喚醒,從而實現了寫優先的操作。
class Global{
    static syn w = new syn(1);			//讓寫程式與其他程式互斥
    static syn rw = new syn(1);			//讀者和寫者互斥訪問共享檔案
    static syn mutex = new syn(1);	//互斥訪問count變數
    static int count = 0;						//給讀者編號
}

寫者程式

/**
 * 寫者程式
 */
class Writer implements Runnable{
	@Override
	public void run() {
		while(true) {
			Global.w.Wait();		//兩個左右,為了寫者的互斥和寫優先(持有w鎖,讓後面的讀程式無法進入)
			Global.rw.Wait();		//互斥訪問共享檔案,如果有讀程式此時正在讀,則會由於缺少rw鎖而在此等待rw.wait()
			/*寫*/
			System.out.println(Thread.currentThread().getName()+"我是作者,我來寫了,現在有"+Global.count+"個讀者還在讀");
			try {
				Thread.sleep(new Random().nextInt(3000));		//隨機休眠一段時間,模擬寫的過程
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"我寫完了");
			Global.rw.Signal();		//釋放共享檔案
			Global.w.Signal();		//恢復其他程式對共享檔案的訪問
			try {
				Thread.sleep(new Random().nextInt(3000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

讀者程式

/**
 * 讀者程式
 */
class Reader implements Runnable{
	@Override
	public void run() {
		while(true) {
			Global.w.Wait();		//為了寫優先,當有寫程式在排隊時,寫程式持有w鎖,之後進入的讀程式由於缺少w鎖資源,會一直等待到寫程式寫完才能獲取w鎖
            Global.w.Signal();		//此時必須釋放,不然就不能保證讀程式之間的併發訪問,因為不釋放,這個程式就會一直持有w鎖,其他讀程式就無法進入
			Global.mutex.Wait();	//互斥訪問count變數
			if(Global.count == 0) {		//進入的是第一個讀者
				Global.rw.Wait();		//佔用rw這個鎖,直到正在進行的所有讀程式完成,才會釋放,寫程式才能開始寫,保證讀寫的互斥
			}	
			Global.count++;		//讀者數量加1
			System.out.println("現在是讀的時間,我是第"+Global.count+"號讀者");
			Global.mutex.Signal();
			
			/*讀*/
			try {
				Thread.sleep(new Random().nextInt(3000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			Global.mutex.Wait();	//互斥訪問count變數
			Global.count--;
			System.out.println("我是第"+(Global.count+1)+"號讀者,我讀完了");
			if(Global.count == 0) {		//最後一個讀程式讀完
				Global.rw.Signal();		//允許寫程式開始寫
			}
			Global.mutex.Signal();	
		}
	}
}

實驗過程遇到的問題

1. 模型的整體梳理

多個讀者和多個寫者同時共享一塊資料區,採取寫優先,讀者與寫者互斥、寫者與寫者互斥。讀者讀的時候可以有別的讀者進來讀,但是一個寫者寫的時候,不允許其他寫者進入來寫,也不允許讀者進來讀,寫者進入的時候必須保證共享區沒有其他程式。

寫程式

在資料區寫資料,用w鎖使得寫者和寫者之間互斥,即一個寫者正在寫的時候,其他寫者無法進入。由於讀者進入時也需呀w鎖,所以會由於未持有w鎖的資源而被加入w鎖的等待佇列w.wait()

寫程式寫的時候需要同時持有w和rw鎖,這樣當有讀者正在讀的時候來了一個寫程式持有w鎖後發現未有rw鎖,進入rw的等待佇列rw.wait(),而自己又持有了w鎖,所以後面來的讀者就會因為缺少w鎖而進入w鎖的等待佇列進行等待,w.wait(),當之前的所有讀程式讀完後釋放rw鎖,這時只有處於rw鎖等待佇列的寫程式能進入資料區寫,這樣就實現了寫優先。

讀程式

在資料區讀資料,進入時需要持有w鎖,然後立即釋放即可。目的是如果有寫程式正在寫(或者正在排隊)就會由於w鎖被寫程式持有而進入等待佇列。同時第一個讀者進入的時候需要拿走rw鎖,目的是告訴外面其他程式有讀程式正在裡面讀,而由於讀程式之間是併發的,所以只需要在第一個讀程式進入時持有rw鎖即可。

2. 等待佇列問題,即寫優先的實現(對去掉讀者w訊號量後出現一直是讀者,幾乎沒有寫者現象的解釋)

去掉讀者的w鎖後,寫優先就無法實現。去掉後讀者進入資料區不再需要持有w鎖,這樣如果此時有三個讀者正在讀,然後有一個寫者請求進入寫資料,由於缺少rw鎖進入rw等待佇列。這時又來了兩個讀者程式請求進入資料區讀資料,由於不用和之前一樣必須持有w鎖,所以就會直接進入資料區開始讀資料,這樣再後面進來的寫者都會進入w鎖等待佇列(w鎖被上一個在rw等待佇列的寫者持有),所以之後將不會再出現寫者,而讀者不受影響,所以之後就只剩讀者程式操作。

3. 讀者順序123開始321結束現象的解釋

原因在於輸出的count值是公有的,當你看到3號讀者進入時,count已經等於3了,這樣後面不管是那個程式結束,輸出時count 都等於3,所以這時候count的值並不能代表是第幾個讀者,而是剩餘讀者的數目。

當第一個讀者進入後拿到mutex,執行count++,然後執行System.out.println("現在是讀的時間,我是第"+Global.count+"號讀者");這句輸出語句,然後釋放mutex,這時CPU切換到第二個讀者,繼續執行之前的步驟,當第三個讀者輸出完這句話時,這時候的count已經等於3了,所以當CPU不論切換到那個讀程式輸出System.out.println("我是第"+(Global.count+1)+"號讀者,我讀完了");這句話,都會從大往小輸出,因為count值是公有的。

3.1 調整

設定一個per類,表示person,裡面有一個count成員,每次count++後,在程式中建立一個per物件,用Global.count初始化,這樣讀者讀完資料輸出自己結束的時候輸出這個執行緒物件的成員count。

class per{
	int count;
	public per(int a) {
		count = a;
	}
}


class Reader implements Runnable{
	@Override
	public void run() {
		while(true) {
			Global.w.Wait();		//在無寫請求時進入
			Global.w.Signal();
			Global.mutex.Wait();	//互斥訪問count變數
			if(Global.count == 0) {		//第一個讀者
				Global.rw.Wait();		//指示寫程式在此時寫
			}	
			Global.count++;		//讀者數量加1
			per per = new per(Global.count);			//用這個物件唯一地標識這個讀者程式
			System.out.println("現在是讀的時間,我是第"+Global.count+"號讀者");
			Global.mutex.Signal();
			
			/*讀*/
			try {
				Thread.sleep(new Random().nextInt(3000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			Global.mutex.Wait();	//互斥訪問count變數
			Global.count--;
			System.out.println("我是第"+per.count+"號讀者,我讀完了");		//通過物件的count成員就知道是第幾個讀者執行緒結束了
			if(Global.count == 0) {		//最後一個讀程式讀完
				Global.rw.Signal();		//允許寫程式開始寫
			}
			Global.mutex.Signal();	//釋放互斥count鎖
		}
	}
}

這時讀者的輸出就會是正常的無序狀態(因為CPU排程是隨機的)。

相關文章