239_读者-写者问题
# 2.3_9_读者-写者问题
各位同学大家好,在这个小节中我们会介绍另一个进程互斥和同步的经典问题,叫做读者写者问题。 假设一个系统当中有读者和写者两组并发进程,他们都共享一个文件,那么对于一个共享文件来说,它可能有一系列的记录组成,所谓记录的概念,在之后学习文件的章节会进一步的学习,这个地方大家只需要知道一个文件,是由一系列的数据单元组成的就可以了。
那么如果说此时有两个读者进程想要同时读这个共享文件的话,那么由于他们读文件操作并不会导致文件的数据发生改变,因此这两个读进程同时读文件其实是不会有任何副作用的。
他们和之前咱们介绍的消费者进程不一样,对于消费者进程来说,他在读数据的时候,其实同时也会把自己读的数据从缓冲距离取走,所以消费者进程是会改变数据的。但是读进程不一样,他们并不会改变这个数据,所以这才说两个或两个以上的读进程,同时访问共享数据时是不会产生副作用的。
另外假设此时是一个写进程和一个读进程,正在同时的访问共享文件,读进程在刚开始可能是在读前三个部分的文件数据,在读完这三个部分之后,他又会紧接着下一个部分的数据,但是他还没读完这三个部分之前写进程就往里边写了一个数据,把这一块的数据给覆盖了。所以读进程在读到这块数据的时候,他本来是想读以前的那块橙色的数据,但是现在变成了绿色这块数据,因此如果说写进程和读进程同时来访问这个共享文件的话,那么有可能导致读进程读到的数据并不是他想读的数据这样的问题,所以写进程和读进程之间,他们是不能同时访问共享文件的。
再来看另外一个问题,如果说是两个写进程同时来访问共享文件的话,这两个写进程在刚开始检查的时候都发现共享文件这块是空的,那么这两个写进程有可能都尝试着往这一块送写入自己想要写的东西,比如说第一个写进程写入了一块橙色的数据,第二个又写入了一块绿色的数据,这就发生了二把第一个写进程的数据给覆盖掉,这样的情况,所以如果说两个写进程同时访问共享文件的话,那么有可能会导致数据的错误覆盖的问题。所以这儿才说某个写进程和其他的所有的进程,同时访问共享数据时,可能会导致数据不一致的错误。也就是说写进程和其他的任何的进程,不论是读进程还是写进程都不能同时访问共享文件。
所以为了防止刚才咱们所说的这些问题,我们有这样的要求。
- 第一,允许多个读者同时对文件执行读操作,因为这样不会产生问题。
- 第二,只允许一个写者往文件里写信息,因为如果说多个写者同时写的话,有可能会产生错误,所以只允许一个写者往里边写信息。
- 第三,任何一个写着在写操作完成之前,是不允许其他读者或者学者工作的,一直要等到写者对文件的写操作结束之后,其他的进程才可以开始对这个文件进行读或者写的操作。
- 第四,写者执行写操作之前,应该先让其他的读者和写者全部退出
这些功能应该怎么用pv操作来实现呢?咱们用之前学过的那一系列的分析方法来依次进行分析,首先这个题目当中很明显有两一进程,另一类是读进程,那么写进程和写进程之间需要实现互斥,写进程和读进程之间也需要实现互斥,但是读进程和读进程之间是不存在互斥问题的,我们怎么用pv操作实现这些互斥关系,实现这些要求,并且要怎么设置信号量呢?
首先写进程和其他的所有的进程都是需要互斥的来访问这个共享文件的,所以为了实现互斥,我们可以设置一个互斥信号量叫做rw,这个信号量是用来表示此时是否有别的进程正在访问共享文件,所以在写进程对共享文件进行访问之前,可以先执行一个p操作,当他对共享文件的访问结束之后,又再执行一个v操作来释放对共享文件加的锁。
另外读进程和写进程之间也需要互斥,所以我们在读进程访问共享文件之前,也需要对互斥信号量执行一个p操作,也需要对共享文件进行加锁,而读完文件之后它也需要进行v操作,也就是解锁,
但是这个地方比较难处理的一个问题是,虽然读者和写者之间是互斥的,但是读者和读者之间是要求可以同时访问共享文件的。那么如果说按照我们刚才所说的这种简单粗暴的做法的话,第一个读者进程在尝试读文件之前执行 p操作,它可以顺利的通过,然后开始读文件,而第二个读进程如果也要执行 p操作的话,那么很显然第二个读进程就会被阻塞,所以这就没办法实现两个读进程,同时访问共享文件这样的事情。
那么这个问题也是读者写者问题最值得我们借鉴的一个问题,它是怎么来处理的呢?从逻辑上来看,对rw这个互斥信号量的pv操作其实可以理解为对共享文件的加锁和解锁。那么我们可以设置一个叫做count的整型变量,用来记录当前到底有几个读进程正在访问这个文件。如果说此时是第一个读进程,想要尝试访问这个文件的话,那么第一个读进程就应该对文件进行加锁。而如果是第二个读进程想要尝试访问这个文件的话,那么我们在经过判断之后,就不应该让第二个读进程再进行一次加锁的操作,应该让第二个读进程直接跳过 p操作,然后直接就可以开始访问这个文件。
我们来看一下具体的代码实现设置一个这叫做rw的互斥信号量,read and write,就是读和写的这个英文单词的两个首字母,那么这个信号量其实就是用来表示当前是否有进程正在访问共享文件,如果是1的话,就意味着此时没有进程,正在访问共享文件,如果小于1的话,就意味着此时已经有别的进程正在访问共享文件了。
那么对于写进程和读进程来说,他们所要做的事无非就是不断的写文件和不断的读文件。在写进程写文件之前一定要对这个共享文件执行加锁的操作,也就是对rw这个信号量执行p操作。
当他写完文件之后,他又需要执行v操作执行解锁,这个很显然写进程和写进程之间是可以实现互斥的,因为第一个写进程可以跳过 p操作,而第二个写进程会暂时被卡在这,直到第一个写进程执行了 v操作之后,第二个写进程才可以被重新激活,然后开始写文件,因此写进程和写进程之间可以实现互斥。
对于读者进程来说,它的操作就要稍微更复杂一些。在他读文件之前,首先要检查此时是不是有别的读进程,也在这进行读文件操作,如果说没有也就是count等于0的话,它作为第一个想要读文件的读进程,它需要对共享文件进行加锁的操作,也就就是对rw执行p操作之后要对count的值加一。在他读完文件之后,又需要对count的值减一,表示此时访问正在读这个文件的进程数减少了一个。之后还需要对 count的值再进行一个检查。如果说他自己是最后一个读文件的读进程的话,那么他又需要对共享文件执行解锁的操作。
如果我们按照这样的方式来处理的话,会发现如果我们此时有两个读进程并发的执行,那么第一个读进程在执行这一句的时候发现 count的值是0,所它可以顺利的通过这个条件判断,然后对rw执行p操作。而在他执行p操作之前,如果说切换回了第二个读进程的话,第二个读进程此时也会发现count的值依然是0,所以它也可以通过这个条件,也可以执行这个p操作。于是两个并发执行的,读进程就有可能先后都执行了,对rw的p操作。很显然第一个读进程可以通过 p操作不会被阻塞,而第二个读进程是会被阻塞在这个地方的。因此如果按这样来做的话,那么读进程和读进程之间有可能并不能实现共同读文件这件事情。
那么我们需要怎么处理呢?其实导致刚才所说的问题的原因在于读进程对于count的检查还有赋值,这两个操作没办法一气呵成,有可能一个读进程在检查完了之后就切换回了另一个读进程,另一个读进程的检查同样也可以通过,所以如果说我们能保证一个读进程的检查和赋值,这两个操作是一气呵成的,那么就不会存在这个问题。
这个地方咱们应该很自然的想到,用一个互斥变量的方式来实现这种一气呵成的事情,我们会设置一个新的互斥变量mutex,在前面和后面分别对mutex执行p和v操作,这也一样对 count的访问之前和访问之后,都需要对mutex分别执行p和v两个操作。
这样的话我们再来分析一下,如果说此时是两个读进程并发执行,那么第一个读进程在可以顺利的通过 p操作,然后开始对count的值进行检查,然后执行 p操作,又执行++的操作。
如果说在执行这些操作的过程当中切换回了第二个读进程的话,第二个读进程会暂时被阻塞在这个地方,不能往下一直到第一个读进程,把这些操作全部完成,然后又执行了v操作之后,第二个读进程才可以顺利的往下执行。
而第二个读进程在执行这句(if count==0)代码的时候,由于第一个读进程已经执行完了++操作,所以此时count的值已经变成了一。因此第二个读进程会发现此时已经有进程正在读文件了,那么第二个读进程就不需要再执行 p操作,他会跳过 p操作,直接执行count++,然后再执行 v操作,最后又开始读文件,所以这就实现了第一个读进程和第二个读进程,同时读共享文件这样的事情。
所以如果我们用这种算法来处理的话,我们是可以实现写者和写者之间互斥,写者和读者之间也互斥,而读者和读者之间不互斥这样的要求的,但是这种算法还存在一个潜在的问题。如果说一个读进程此时正在读文件,那么此时如果有源源不断的读进程进入这个系统的话,那么那些读进程其实都可以跳过前面的这些操作,然后都可以开始读文件。如果说有源源不断的读进程到来,此时如果一个写进程是被阻塞在这个地方的话,写进程可能永远都不会被唤醒,它会一直处于阻塞的状态,所以就可能会发生饿死或者说饥饿的现象。所以其实这个算法当中我们是默认读进程是优先的
那么我们要怎么解决这个问题?我们可以设置一个新的斥信号量叫做w,然后对在这些地方分别对新的互斥信号量执行p和v的操作。我们来分析一下,如果按这样的算法的话会发生什么情况?
如果说是两个读者进程并发的执行,那么第一个读者进程在这可以顺利的通过 p,也可以顺利的通过第二个p,然后一直处理完这些东西,并且对rw进行上锁之后,他就可以顺利的开始读文件,
而由于第一个读者进程已经对这两个互斥信号量执行了,都分别执行了v操作,所以如果第二个读者进程到达的话,他也可以顺利的通过这些操作,并且也开始读文件,所以如果加上w的这两个pv操作的话,那么读者和读者之间可以同时读文件,这件事情是不会被影响的。
另外如果是两个写者并发的执行的话,那么第一个写者可以顺利的通过这两个p操作,然后开始写文件,而第二个写者在执行到第一个p操作的时候,他就会被阻塞,一直到第二个写者写完文件,并且对w互斥信号量执行了v操作之后,第二个写者才可以跳过阻塞,然后又继续开始写文件,所以如果加上了这两个条件的话,那么写者者和写者之间的互斥也是不会受影响的。
第三,我们再来看一下写者和读者两个进程并发执行的话会发生什么情况。首先一个写者进程顺利的执行了这两个p操作,然后开始写文件,而读者进程在执行到 p(w)操作的时候,它就会被阻塞,一直要等待写者进程写完文件,并且对w这个互斥信号量执行了v操作之后,读者进程才可以跳过这个阻塞,然后顺利的开始往下执行,顺利的开始读文件,所以读者和写者这两种进程的互斥其实也不会受到影响。
接下来我们再来看一下这种算法还会不会导致写者饥饿的现象。如果一个读者正在读这个文件的话,那它肯定是顺利的执行了上面的这些操作包括对rw这个互斥信号量的p操作。此时如果说他在读文件的过程中有一个新的写者进程到达,由于由于第一个读者进程已经对w信号量执行过了v操作,所以写者进程执行 p 操作的时候不会被阻塞。在执行对rw的p操作的时候,由于第一个读者进程已经对rw执行了一个p操作也就是加锁,所以写者进程会被阻塞在rw这个变量这。
此时如果有第二个读者进程到达的话,由于之前读者进程已经对w执行了p操作,但是他又没有执行v操作,所以读者进程他在执行 p 操作的时候就会被阻塞在这个地方,没办法继续往下。一直到第一个读进程,读完文件之后他又进行了count--,发现count的值变为了0,于是他又会对rw这个互斥信号量执行v操作,所以写进程本来是阻塞在这个地方的,当读进程执行了v操作之后,写进程就可以被唤醒,然后可以开始顺利的写文件,而第二个读进程依然是被阻塞在这个地方。
所以有没有发现如果采用这样的算法的话,如果一个读进程在读的过程当中有写进程到达了,那么之后是会优先让写进程访问共享文件的,而不是像之前的算法一样,只要有读进程到达,那么读进程就会插到写进程之前,然后就可以在写进程之前开始访问共享文件,所以这就避免了咱们之前说到的写进程会一直饥饿的问题。
接下来我们再来看另一种情况,如果说是一个写进程先执行,那么写第一个写进程肯定可以通过这两个p操作开始写文件。而第二个到达的进程,如果是一个读进程的话,那么读进程它首先是会被阻塞在这个地方,也就是p(w)这个变量这。而此时如果第二个写进程到达的话,第二个写进程也会被阻塞到这个地方,也就是需要等待w互斥信号量被释放,但是由于读者进程它是先到达的,所以当第一个写进程执行完写操作,并且对w执行v操作之后,那么这个v操作唤醒的进程应该是先对w信号量执行p操作的进程,也就是读者进程一所以这个地方的写优先并不是真正意义上的写优先,这种算法实际上是相对公平的这种先来先服务的原则,所以有的书上又把这种算法称为读写公平法。
# 小结
那么读者写者问题其实给我们提供了一个思路,它其实只包含互斥的进程互斥的问题,只不过比起咱们之前介绍的那些来说,它的互斥关系要更复杂一些。因为读者和写者之间需要互斥,但是读者和读者之间是不需要互斥的,所以它的核心思想在于我们设计了一个叫做count的计数器,用来记录当前正在访问共享文件的进程数量到底是多少,那么我们就可以用 count的值来判断此时是不是第一个读进程在尝试访问共享文件,如果是第一个读进程的话,那么就需要对共享文件进行加锁。如果是最后一个读进程的话,那么当他读完文件之后,就可以都就需要对共享文件进行解锁。
但是之前咱们遇到了一个问题,就是由于count的检查和赋值这两个操作没办法一起成,所以就导致了一些意想不到的错误。那么如果说以后遇到这种需要实现一气呵成的这种问题的话,那么我们自然应该想到使用互斥信号量的方式来进行解决。
另外我们还需要注意体会我们是怎么解决写进程饥饿的问题的,我们新增加了一个叫做w的互斥信号量,并且在合适的位置对这个信号量执行了pv操作,那么在咱们考研当中遇到的那些pv操作的大题,其实大部分都可以用生产者消费者问题的那些思想来解决,但是如果遇到了比较复杂的那种问题,特别是像这种有的进程之间需要互斥,但是进程和自己本身之间又不需要互斥这种问题的时候,我们就需要想到能不能用读者写者的这种思想,也就是count计数器这种思想来解决问题。