操作系统实验——读者写者模型(写优先)
个人博客主页
参考资料:
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调度是随机的)。