操作系統實驗——讀者寫者模型(寫優先)
個人博客主頁
參考資料:
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調度是隨機的)。