经典的进程同步问题-----读者-写者问题详解
本文和接下来几篇博文是对上篇文章(进程同步机制)的一次实践,通过具体的例子来加深理论的理解,会用三个经典的进程同步问题来进行讲解,并且会配有伪代码和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的干货(目录),喜欢的话还请点赞、评论加关注^_^。
参考文章列表: