读者写者问题

读者写者问题的基本要求:

  1. 共享读
  2. 写写互斥
  3. 读写互斥

读写问题对应着一种真实的工程需求,引入这个问题之后,用一个信号量mutex粗暴互斥所有访问临界资源的进程行不通了。爷青结。从上述读者写者问题的基本要求可以看出,不能将读写进程全部互斥,因为读临界资源的操作显然是可以共享的,全部互斥的做法在读进程很多的情况下势必造成临界资源利用率降低,是很不优美的。

由此自然的想到一个解决方法(也没有那么自然),就是设置一个信号量authority,初始值为1,单个写进程与读进程的集合争夺authority资源。即当一个写进程占用了authority后,其他写进程和读进程都被阻塞,但当一个读进程占用了authority后,读进程没有被阻塞,写进程被阻塞。

在这样的设计思想下,authority的一个子功能是使得写进程互斥,另一个子功能是使得读写互斥。利用第一个子功能可以先将写进程写成如下形式。

semaphore authority = 1;
writer() {
P(authority);
write();
V(authority);
}

如刚才所说,这样的写进程设计已经能完成写进程的互斥了。接下来考虑一下读进程的问题。读进程有一点困难,我们先搭一个大致结构。读进程如下

reader() {
P(authority);
read();
V(authority);
}

大家可能也发现了,这不就是读读,读写,写写都互斥吗?无情!这显然是不满足要求的,那我们来做一些修改。

首先,如果此时authority资源被写进程占用,此时必定没有读进程处于读的状态,此时的读进程应该被阻塞在P(authority)的位置。但由于同时有很多个读进程,不能将他们全部阻塞在authority中,因为按照我们的设计,只有第一个读进程需要取得authority,而其他的读进程在已经有读进程取得authority的情况下应该畅通无阻。即第一个进程阻塞在P(authority),其他进程另行阻塞。那么这些读进程应该阻塞在哪里呢?这是第一个问题。

其次,如果此时authority资源被读进程占用,进程读完之后直接释放authority好像也不合适,因为可能有其他的读进程

这就引出了读者写者问题的第二个重要设计readcount.

readcount用于表示此时正在读的进程的数量,初值为0,由于这个变量需要在多个进程中共享,其显然不是一个局部变量,那我们让他成为一个全局变量。readcount能解决什么问题呢?就是上面说的识别第一个读进程和最后一个读进程的问题。读到这里,有没有觉得少了些什么?

没错,那就是readcount作为所有reader的临界资源,需要一个mutex互斥访问,否则可能造成混乱。

有了全局计数readcount之后,我们来看如下的设计

semaphore authority = 1, mutex = 1;
int readcount = 0;

writer() {
P(authority);
write();
V(authority);
}

reader() {
   P(mutex);
readcount++;
if (readcount == 1) {//如果是第一个读进程
P(authority);
}
   V(mutex);
read();
   P(mutex);
readcount--;
if (readcount == 0) {//最后一个结束的读进程
V(authority);
}
   V(mutex);

}

关于mutex的作用范围还有一点提示。由于readcount在自增和自减后都紧跟着一个判断,这个判断必须和自增自减在一组对应的PV操作中,大家可以思考一下为什么。

至此,我们可以分析一下上述设计有没有达到我们的目标:

  1. 共享读:当已经有一个读进程获得authority的时候,其余的读进程都不会被authority阻塞,但需要改变临界资源readcount;完成读操作后,读进程改变readcount后就结束了,除了最后一个进程需要释放authority资源。
  2. 写写互斥:显然,如上文所说的authority的子功能之一。
  3. 读写互斥:显然。

这是运用信号量解决同步互斥问题的经典问题,也是本文章的第一部分,它的实现给了我们如下提示:

  1. 设立一个全局的计数变量,可以实现对进程的计数,往往用于对第一个或者最后一个进程做特殊的操作,需要配套一个mutex进行保护。
  2. 当一组PV操作嵌套时,可以实现在某种特定的情况下,进程阻塞在不同的信号量。如写进程获得authority资源时,获得了mutex的读进程(可以理解为第一个读进程)被阻塞在了P(authority),而其他的读进程被阻塞在了mutex
  3. 读进程的共享模式可以理解为开关灯的模式。第一个进房间的人开灯,最后一个离开房间的人关灯。

写者优先的读者写者问题

你是否有很多的问号?难道读者写者问题还分谁优先?

确实。第一部分的实现是读者写者的一种实现方法,通常被定义为读者优先的写法。我们设想这样一个场景,春运时的购票系统,上万人访问同一辆列车的资源,他们都是读者,可以共享访问。而一旦有人购票,就会有一个写者的进程。现在读者获取了authority,但读者源源不断,始终停不下来,那么写进程就一直被阻塞,你的票就很难买到。所以第一种实现虽然较为直观,但会造成写者饿死的情况。值得注意的是,这里所说的读者优先并不是完全的优先,当一个写进程和一个读进程同时等待一个写进程释放authority的时候,他们是平等竞争的。

为了避免写者饿死的情况,有必要给出一种新的读者写者问题的解法,这就是我们下面要将的写者优先解法。

写者优先的读者写者问题的基本要求:

  1. 共享读
  2. 写写互斥
  3. 读写互斥
  4. 写者优先于读者 (一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。

新增了第4条,强调了唤醒时的优先级,这是之前没有的。

我们来分析一下。假设现在有多个写进程,那么当第一个写进程结束,释放临界资源后,另一个写进程就会跟上,在写进程没有结束之前,都不会轮到读进程,最后一个写进程结束需要通知读进程。这很像第一种实现中的authority,只不过主动权掌握在了写进程手里。那我们就先给写进程配上wirtecountmutex_w。由于这时authority失去了写写,读写互斥的功能,故加入一个mutex用来表示对临界资源的访问,authority就只负责写的特权。

semaphore authority = 1, mutex_r = 1, mutex_w = 1, mutex = 1;
int readcount = 0, writecount = 0;

writer() {
   P(mutex_w);
   writecount++;
   if (writecount == 1) {
       P(authority);
  }
V(mutex_w);
   P(mutex);
write();
   V(mutex);
P(mutex_w);
   writecount--;
   if (writecount == 0) {
       V(authority);
  }
V(mutex_w);
}

reader() {
   P(mutex_r);
readcount++;
if (readcount == 1) {//如果是第一个读进程
P(mutex);
}
   V(mutex_r);
read();
   P(mutex_r);
readcount--;
if (readcount == 0) {//最后一个结束的读进程
V(mutex);
}
   V(mutex_r);

}

现在authority还没有发挥作用,我们需要对reader做一些修改。我们需要实现当出现写者进程的时候,阻塞读者进程,即已经进行中的读进程让他们执行完,释放authority,而晚于写进程出现的读进程,使其阻塞在readcount++之前(如此便能让readcount == 0时释放mutex

semaphore authority = 1, mutex_r = 1, mutex_w = 1, mutex = 1;
int readcount = 0, writecount = 0;

writer() {
   P(mutex_w);
   writecount++;
   if (writecount == 1) {
       P(authority);
  }
V(mutex_w);
   P(mutex);
write();
   V(mutex);
P(mutex_w);
   writecount--;
   if (writecount == 0) {
       V(authority);
  }
V(mutex_w);
}

reader() {
P(authority);
   P(mutex_r);
readcount++;
if (readcount == 1) {//如果是第一个读进程
P(mutex);
}
   V(mutex_r);
   V(authority);
read();
   P(mutex_r);
readcount--;
if (readcount == 0) {//最后一个结束的读进程
V(mutex);
}
   V(mutex_r);

}

我们来分析一下现在能达到什么效果。理解的核心仍然是authority。我们考虑第一个写进程W1获取authority后,恰有一读进程R1和写进程W2开始。

W1获得authority,故R1被阻塞,W2先后获取W1释放的mutex_wmutex,待W2释放authority后,R1才进行下去。writer确实优先。

然而当读者获得authority,读者和写者都被阻塞在P(authority)时,读者和写者还是一种平等竞争关系。这种写者优先与上面讲的读者优先一样,在笔者看来都是“不完全的优先”。于是又进行了如下的修改。

semaphore authority = 1, mutex_r = 1, mutex_w = 1, mutex = 1;
int readcount = 0, writecount = 0;

writer() {
   P(mutex_w);
   writecount++;
   if (writecount == 1) {
       P(authority);
  }
V(mutex_w);
   P(mutex);
write();
   V(mutex);
P(mutex_w);
   writecount--;
   if (writecount == 0) {
       V(authority);
  }
V(mutex_w);
}

reader() {
   P(mutex_r);
   P(authority);
readcount++;
if (readcount == 1) {//如果是第一个读进程
P(mutex);
}
V(authority);
   V(mutex_r);
   
read();
   P(mutex_r);
readcount--;
if (readcount == 0) {//最后一个结束的读进程
V(mutex);
}
   V(mutex_r);

}

做一个调换顺序的尝试,我们来看看这样的修改能否满足要求。由于只有一个读进程可以进入mutex_r保护的区域,故在authority上最多只能阻塞一个进程,且读进程释放authority时,没有读进程阻塞在authority。看似很美好,但我们设想一下这个场景:读进程R1释放authority,此时读进程R2阻塞在一开始的mutex_r,写进程W1阻塞在authority,故写进程获得authority。然而,画风突变,W1手握authority:“我的mutex呢?”R1刚刚执行read()结束,手握mutex:“我的mutex_r呢?”R2手握mutex_r,卡在P(authority),意识到问题并不简单......

出现死锁,说明这个尝试并不成功。那还有什么办法让authority最多阻塞一个进程吗?答案是有的,再加一个信号量,直接上代码。

semaphore authority = 1, mutex_r = 1, mutex_w = 1, mutex = 1, queue = 1;
int readcount = 0, writecount = 0;

writer() {
   P(mutex_w);
   writecount++;
   if (writecount == 1) {
       P(authority);
  }
V(mutex_w);
   P(mutex);
write();
   V(mutex);
P(mutex_w);
   writecount--;
   if (writecount == 0) {
       V(authority);
  }
V(mutex_w);
}

reader() {
   P(queue);
   P(authority);
   P(mutex_r);
readcount++;
if (readcount == 1) {//如果是第一个读进程
P(mutex);
}
   V(mutex_r);
   V(authority);
   V(queue);
read();
   P(mutex_r);
readcount--;
if (readcount == 0) {//最后一个结束的读进程
V(mutex);
}
   V(mutex_r);

}

我让所有的reader进程阻塞在queue中,这样每次只有一个读进程进入queue保护的区域,自然不会有读者与写者争抢authority

最后一个问题。我们在读者进程中考虑authority的位置时只考虑了P(authority)的位置,但没有考虑V(authority)的位置,如果我们做如下的改动,会有什么影响呢?

reader() {
   P(queue);
   P(authority);
   V(authority);
   V(queue);
   P(mutex_r);
readcount++;
if (readcount == 1) {//如果是第一个读进程
P(mutex);
}
   V(mutex_r);
   read();
   P(mutex_r);
readcount--;
if (readcount == 0) {//最后一个结束的读进程
V(mutex);
}
   V(mutex_r);

}

修改后的方案似乎是可行的,但笔者搜集资料得到的都是第一种处理方法。我们来分析一下修改后的方案。reader进程在queue的位置排队,每次进入一个读进程,尝试获取authority,获得后立即释放,让下一个读进程尝试获取authority,此时若出现写进程尝试获取authority,下一个(如果有)读进程就会阻塞在P(authority),写进程紧接着要获取mutex,参与计数的读进程运行完毕后,就会释放mutex,好像没什么不行。但笔者仍然找到一个区别。修改后的放在在进程顺序为R1-W1-R2时,无法确定R1的读和W1的写哪一个先发生(由于R1提早释放authority,R1会与W1争抢mutex),而修改前的方案在这种情况下是确定的,一定是R1先获得mutex,故修改前的设计可以作为写者优先的读写问题的一种解答。

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值