作業系統實驗——讀者寫者模型(寫優先)
個人部落格主頁
參考資料:
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排程是隨機的)。