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

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

個人博客主頁
參考資料:
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調度是隨機的)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章