1. 程式人生 > >作業系統實驗——讀者寫者模型(寫優先)

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

# 作業系統實驗——讀者寫者模型(寫優先) [個人部落格主頁](http://www.vfdxvffd.cn) 參考資料: [Java實現PV操作 | 生產者與消費者](https://www.cnblogs.com/TQCAI/p/7700354.html)
## 讀者寫者 對一個公共資料進行寫入和讀取操作,和之前的生產者消費者模型很類似,我們梳理一下兩者的區別。 * 都是多個執行緒對同一塊資料進行操作 * 生產者與生產者之間互斥、消費者與消費者之間互斥、生產者與消費者之間互斥 * 寫者與寫者之間互斥、讀者與寫者之間互斥、但讀者與讀者之間併發進行 寫優先是說當有讀者進行讀操作時,此時有寫者申請寫操作,只有等到所有正在讀的程序結束後立即開始寫程序 ## 定義PV操作 ```java /** * 封裝的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()```,此時也只有那個等待的寫者程序可以被喚醒,從而實現了寫優先的操作。 ```java 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; //給讀者編號 } ``` ## 寫者程序 ```java /** * 寫者程序 */ 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(); } } } } ``` ## 讀者程序 ```java /** * 讀者程序 */ 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。 ```java 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排程是隨機