经典的进程同步问题-----读者-写者问题详解

经典的进程同步问题-----读者-写者问题详解

​ 本文和接下来几篇博文是对上篇文章(进程同步机制)的一次实践,通过具体的例子来加深理论的理解,会用三个经典的进程同步问题来进行讲解,并且会配有伪代码和Java实践(使用多线程模拟),深入的进行讲解。

​ 进程同步问题是一个非常重要且相当有趣的问题,本文我们对其中比较有名的读者-写者问题来进行探讨。读者-写者问题是指保证一个Writer进程必须与其他进程互斥地访问共享对象的同步问题。也因为其问题较为复杂,其进程被用来测试新的同步原语,因此,本文对读者-写者问题来进行分析。

1.问题描述

​ 一个数据文件或者记录可被多个进程共享,我们把只要求读文件的进程称为“Reader”进程,其他进程则称为“Writer”进程。允许多个进程同时读一个共享对象,因为读操作不会使数据文件混乱。但是不允许一个Writer进程和其他的Reader进程或Writer进程同时访问共享对象(因为这种访问会引起数据的混乱)。

​ 也就是读者-写者问题要求:

  1. 允许多个读者同时执行读操作;
  2. 不允许读者、写者同时操作;
  3. 不允许多个写者同时操作。

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的干货(目录),喜欢的话还请点赞、评论加关注^_^。

参考文章列表:

1.进程同步机制-----为进程并发执行保驾护航

2.Java并发编程(JUC)模拟AND型信号量

3.Java并发编程(JUC)模拟信号量集

4.Java并发编程模拟管程(霍尔Hoare管程、汉森Hansan管程、MESA管程)

5.操作系统武功修炼心法

6.经典进程同步问题----生产者-消费者问题详解

7.经典进程同步问题----哲学家进餐问题详解

发布了27 篇原创文章 · 获赞 212 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_34666857/article/details/103232078