操作系统实验——读者写者模型(写优先)

操作系统实验——读者写者模型(写优先)

个人博客主页
参考资料:
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调度是随机的)。

posted @ 2020-09-13 10:40  头发是我最后的倔强  阅读(1461)  评论(0编辑  收藏  举报