經典的進程同步問題-----讀者-寫者問題詳解
本文和接下來幾篇博文是對上篇文章(進程同步機制)的一次實踐,通過具體的例子來加深理論的理解,會用三個經典的進程同步問題來進行講解,並且會配有僞代碼和Java實踐(使用多線程模擬),深入的進行講解。
進程同步問題是一個非常重要且相當有趣的問題,本文我們對其中比較有名的讀者-寫者問題來進行探討。讀者-寫者問題是指保證一個Writer進程必須與其他進程互斥地訪問共享對象的同步問題。也因爲其問題較爲複雜,其進程被用來測試新的同步原語,因此,本文對讀者-寫者問題來進行分析。
1.問題描述
一個數據文件或者記錄可被多個進程共享,我們把只要求讀文件的進程稱爲“Reader”進程,其他進程則稱爲“Writer”進程。允許多個進程同時讀一個共享對象,因爲讀操作不會使數據文件混亂。但是不允許一個Writer進程和其他的Reader進程或Writer進程同時訪問共享對象(因爲這種訪問會引起數據的混亂)。
也就是讀者-寫者問題要求:
- 允許多個讀者同時執行讀操作;
- 不允許讀者、寫者同時操作;
- 不允許多個寫者同時操作。
2.問題分析
我們按照準備訪問共享對象的進程種類來進行問題的分析:
如果Reader進程準備訪問共享對象,當前系統中分爲以下幾種情況:
1)無Reader、Writer,這個新Reader可以讀;
2)有Writer等,但有其它Reader正在讀,則新Reader也可以讀;
3)有Writer寫,新Reader等待。
如果Writer進程準備訪問共享對象,當前系統中分爲以下幾種情況:
1)無Reader、Writer,新Writer可以寫;
2)有Reader,新Writer等待;
3)有其它Writer,新Writer等待。
3.信號量設置
設置一個整型變量readcount表示正在讀的進程數目,該變量是可被多個讀進程訪問的臨界資源;
wmutex用於讀者和寫者、寫者和寫者進程之間的互斥;
rmutex用於對readcount這個臨界資源的互斥訪問。
4.使用記錄型信號量解決讀者-寫者問題
通過上面的分析,我們直接給出解題的僞代碼:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class ReaderWriterTest {
static Semaphore rMutex = new Semaphore(1);
static Semaphore wMutex = new Semaphore(1);
static int readCount = 0;
//讀者
static class Reader extends Thread {
Reader(String name) {
super.setName(name);
}
@Override
public void run() {
do {
try {
//操作readCount,需要先進入臨界區
rMutex.acquire();
//判斷在當前時刻,該讀進程是否是系統中唯一的讀者
if(readCount == 0){
wMutex.acquire();
}
//系統中的讀者數量加1
readCount ++;
rMutex.release();
log.info("讀者【{}】在執行讀操作,當前讀者數:【{}】", getName(), readCount);
Thread.sleep(5000);
//操作readCount,需要先進入臨界區
rMutex.acquire();
readCount --;
//如果該讀者是否是系統中最後離開的,則需要喚醒寫者
if(readCount == 0){
wMutex.release();
}
rMutex.release();
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("哲學家執行時產生異常!");
}
} while (true);
}
}
//寫者
static class Writer extends Thread {
Writer(String name) {
super.setName(name);
}
@Override
public void run() {
do {
try {
//判斷進入臨界區
wMutex.acquire();
log.info("寫者【{}】執行了寫操作", getName());
Thread.sleep(1000);
wMutex.release();
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("寫進程執行時產生異常!");
}
} while (true);
}
}
public static void main(String[] args) {
Reader r1 = new Reader("r1");
Reader r2 = new Reader("r2");
Reader r3 = new Reader("r3");
Writer w1 = new Writer("w1");
Writer w2 = new Writer("w2");
r1.start();
r2.start();
r3.start();
w1.start();
w2.start();
}
}
對於讀進程中的if(readcount == 0) p(wmutex),這是因爲讀者和寫者之間的關係決定的,因爲讀者到達且爲當前時刻t1系統中的第一個讀者,所以需要讓寫進程無法進入臨界區。這裏,還有一個精妙的設計,就是如果在t1時刻,已經有寫者在操作共享對象,此時第一個讀者來,去申請wmutex信號量,必定會因爲資源不足而阻塞,這裏通過一個wmutex來控制讀者和寫者的同步,可以說設計的非常精妙了。
5.使用信號量集解決讀者-寫者問題
對於上面分析的部分,如果寫者在寫,讀者需要等待,這裏我們回顧一下信號量集的操作,並且我們在這篇文章中使用Java模擬了信號量集,並且通過信號量集,可以很方便的限制同時進行讀操作的讀者的數量,下面試對應的僞代碼:
semaphore rmutex = N, wmutex = 1; //初始化信號量,N爲同一時刻最大的讀者數
void Reader(){
do {
Swait(rmutex,1,1,wmutex,1,0); //判斷讀者數量是否大於或等於N&&是否有寫者在操作
//...
//read //執行讀操作
//...
Ssignal(rmutex,1); //釋放信號量
}while(true);
}
void Writer(){
Swait(wmutex,1,1,rmutex,N,0); //判斷是否有讀者或者寫者在操作
//...
//write //執行寫操作
//...
Ssignal(wmutex,1); //釋放信號量
}
其中Swait(wmutex,1,0)語句起着開關的作用,其中的資源下限爲1,只有當前沒有寫進程在操作共享對象時wmutex的值才爲1,否則爲0,也就說,只有wmutex=1時,Reader纔可進行讀操作,否則只能等待。另外,Swait(wmutex,1,1,rmutex,N,0)也可以作爲一個開關,其中的rmutex的資源下限爲N,wmutex的資源下限爲1,即只有當前系統中一個Reader和Writer都不存在是,Writer纔可以進行寫操作。
6.測試
這裏我們通過Java解決讀者-寫者問題,這裏我們使用方法一(方法二可參考我的另一篇模擬實現信號量集的文章,將其中的Swait操作和Ssignal操作實現即可),下面是具體的代碼:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
public class ReaderWriterTest {
static Semaphore rMutex = new Semaphore(1);
static Semaphore wMutex = new Semaphore(1);
static int readCount = 0;
static class Reader extends Thread {
Reader(String name) {
super.setName(name);
}
@Override
public void run() {
do {
try {
rMutex.acquire();
if(readCount == 0){
wMutex.acquire();
}
readCount ++;
//log.info("讀者【{}】在讀操作執行結束,當前讀者數:【{}】", readCount);
rMutex.release();
log.info("讀者【{}】在執行讀操作,當前讀者數:【{}】", getName(), readCount);
Thread.sleep(5000);
rMutex.acquire();
readCount --;
if(readCount == 0){
wMutex.release();
}
rMutex.release();
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("哲學家執行時產生異常!");
}
} while (true);
}
}
static class Writer extends Thread {
Writer(String name) {
super.setName(name);
}
@Override
public void run() {
do {
try {
wMutex.acquire();
log.info("寫者【{}】執行了寫操作", getName());
Thread.sleep(1000);
wMutex.release();
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("寫進程執行時產生異常!");
}
} while (true);
}
}
public static void main(String[] args) {
Reader r1 = new Reader("r1");
Reader r2 = new Reader("r2");
Reader r3 = new Reader("r3");
Writer w1 = new Writer("w1");
Writer w2 = new Writer("w2");
r1.start();
r2.start();
r3.start();
w1.start();
w2.start();
}
}
下面是代碼的執行結果(這裏的結果如此有序,要感謝上面代碼中的sleep):
又到了分隔線以下,本文到此就結束了,本文內容全部都是由博主自己進行整理並結合自身的理解進行總結,如果有什麼錯誤,還請批評指正。
本文的java代碼都已通過測試,對其中有什麼疑惑的,可以評論區留言,歡迎你的留言與討論;另外原創不易,如果本文對你有所幫助,還請留下個贊,以表支持。
如有興趣,還可以查看我的其他幾篇博客,都是OS的乾貨(目錄),喜歡的話還請點贊、評論加關注^_^。
參考文章列表: