OS老师上课在讲解读者和写者问题时,一般会按照三个层次展开讲解。根据信号量的分布和控制逻辑,大体可以将读者-写者问题按由易到难分成如下这三大类:
本文将从这三类中选择关键的例子以“注解+代码”的形式按序剖析。
rw用于实现写写互斥,读写互斥,但也导致了读读互斥。
ReaderCnt的相关操作在写写互斥和读写互斥的基础上实现了读读不互斥,同时也实现了占位后读优先。
mutex1用于对ReaderCnt的读写操作提供互斥保护。
int ReaderCnt=0;
Semaphore mutex1=1, rw=1;
Reader(){
P(mutex1);
ReaderCnt++;
if(ReaderCnt==1) P(rw);
V(mutex1);
Reading();
P(mutex1);
ReaderCnt--;
if(ReaderCnt==0) V(rw);
V(mutex1);
}
Writer(){
P(rw);
Writing();
V(rw);
}
w用于在写的最外层再建立一个父队列,这样rw的队列中不会出现写。不论之前到达了多少写,任意首次到达的读均能够直接被放到rw首部,即"总能跳过所有写"。待正在执行的写进程退出后,立刻执行这个首次到达的读,即读占位,从而实现占位前优先。
int ReaderCnt=0;
Semaphore mutex1=1, rw=w=1;
Reader(){
P(mutex1);
ReaderCnt++;
if(ReaderCnt==1) P(rw);
V(mutex1);
Reading();
P(mutex1);
ReaderCnt--;
if(ReaderCnt==0) V(rw);
V(mutex1);
}
Writer(){
P(w);
P(rw);
Writing();
V(rw);
V(w);
}
q用于实现一个读和写的FIFO公共队列,这个队列中进程的顺序与进程的到达次序一致,从而实现FCFS,即读写公平。
Reader()中V(q)的位置很巧妙:首先,V(q)应在V(mutex1)之后,这是由对称性决定的,因为P(q)恰好在P(mutex1)之前,这个对称性一旦被破坏,会进一步使mutex1的功能也被破坏;另一方面,Reading()允许并行执行,若把V(q)放到Reading()之后将会破坏这一条件,使得读不能并行。综上,V(q)应放在第一个V(mutex1)和Reading()之间。
扩展:如果把这里的Reading()换成Writing(),那么V(q)的位置可以放到Writing()之后,或者Reader()的最末尾。因为Writing()本来就不能并行,上段中的第二个位置约束不再成立。
int ReaderCnt=0;
Semaphore mutex1=1, rw=q=1;
Reader(){
P(q);
P(mutex1);
if(ReaderCnt==0) P(rw);
ReaderCnt++;
V(mutex1);
V(q);
Reading();
P(mutex1);
ReaderCnt--;
if(ReaderCnt==0) V(rw);
V(mutex1);
}
Writer(){
P(q);
P(rw);
Writing();
V(rw);
V(q);
}
类似地,ReaderCnt和wpao实现占位后优先,wpbo实现占位前优先。
wpbo = Write Priority Before Occupation,wpao = Write Priority After Occupation。
int ReaderCnt=0, WriterCnt=0;
Semaphore mutex1=mutex2=1, rw=wpbo=wpao=1;
Reader(){
P(wpbo);
P(wpao);
P(mutex1);
ReaderCnt++;
if(ReaderCnt==1) P(rw);
V(mutex1);
V(wpao);
V(wpbo);
Reading();
P(mutex1);
ReaderCnt--;
if(ReaderCnt==0) V(rw);
V(mutex1);
}
Writer(){
P(mutex2);
WriterCnt++;
if(WriterCnt==1) P(wpao);
V(mutex2);
P(rw);
Writing();
V(rw);
P(mutex2);
WriterCnt--;
if(WriterCnt==0) V(wpao);
V(mutex2);
}