读者写者问题,其本质就是连续多个同类进程访问同一个临界资源的问题。
第一个进程开始访问临界资源前,需要对资源加上互斥锁,后面的进程再访问时就不用再对资源加互斥锁了,直到最后一个进程访问完后,发现自己是最后一个进程,就解锁互斥锁。这就像一种情况:第一个人进房间时必须顺手开门,后面进来的人和离开的人就不用开门,直到最后一个人离开房间时才需要顺手关门。
代码的通用模板是“三段式”,如下:
int count = 0; // 记录正在访问的进程数量
信号量 busy = 1; // “完成事件”的互斥锁
信号量 mutex = 1; // 变量 count 的互斥锁
Process(){
while(1){
P(mutex);
count++; // 访问资源的进程数量加 1
if (count == 1){ // (1)如果发现自己是第一个访问的进程,需要负责加锁
P(busy);
}
V(mutex);
完成事件; // (2)访问临界资源、完成临界事件
P(mutex);
count--; // 访问完毕,访问资源的进程数量减 1
if (count == 0){ // (3)如果发现自己是最后一个访问的进程,需要负责解锁
V(busy);
}
V(mutex);
}
}
理解了最基本的原理后,下面来正式讨论读者写者问题。
【读者写者问题】有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程(只是读数据,不会对数据产生影响,而消费者读数据时,会将数据取走,因此不能两个消费者一起读数据)同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。
因此要求:
即使写者发出了请求写的信号,但是只要还有读者在读取内容,就还允许其他读者继续读取内容,直到所有读者结束读取,才真正开始写。
int count = 0;
信号量 busy = 1; // “读文件”和“写文件”的互斥锁
信号量 mutex = 1; // 变量 count 的互斥锁
Reader(){ // 读者进程
while(1){
P(mutex);
count++;
if (count == 1){
P(busy);
}
V(mutex);
读文件;
P(mutex);
count--;
if (count == 0){
V(busy);
}
V(mutex);
}
}
Writer(){ // 写者进程
while(1){
P(busy);
写文件;
V(busy);
}
}
如果读者写者到达的顺序是:读者 1–读者2–读者 3–写者 A–读者 4–写者 B–读者 5
,则:
读写进程都要排队进行操作文件。即使里面有读进程在操作文件,读进程也要和写进程一起排队。
int count = 0;
信号量 queue = 1; // 实现“读写公平”的互斥锁,可以视为一个队列
信号量 busy = 1; // “读文件”和“写文件”的互斥锁
信号量 mutex = 1; // 变量 count 的互斥锁
Reader(){ // 读者进程
while(1){
P(queue); // 在无写进程请求时不需要进入队列
P(mutex); // 该互斥量实际上是多余的,上面语句已经兼有互斥功能
count++;
if (count == 1){
P(busy);
}
V(mutex); // 该互斥量实际上是多余的,下面语句已经兼有互斥功能
V(queue); // 恢复对共享文件的访问
读文件;
P(mutex);
count--;
if (count == 0){
V(busy);
}
V(mutex);
}
}
Writer(){ // 写者进程
while(1){
P(queue); // 在无其他写进程请求时不需要进入队列
P(busy);
写文件;
V(busy);
V(queue); // 恢复对共享文件的访问
}
}
如果读者写者到达的顺序是:读者 1–读者2–读者 3–写者 A–读者 4–写者 B–读者 5
,则:
实际上,读写公平法也可以不用任何除了访问文件外的互斥锁:
信号量 busy = 1; // 也可以视为一个队列
Reader(){ // 读者进程
while(1){
P(busy);
读文件;
V(busy);
}
}
Writer(){ // 写者进程
while(1){
P(busy);
写文件;
V(busy);
}
}
如果有写者申请写文件,那么在申请之前还在读取文件的读进程可以继续读取,但是如果再有读者申请读取文件,则不能够读取,只有在所有的写者写完之后才可以读取。
int ReaderCount = 0; // 读者数量
int WriterCount = 0; // 写者数量
信号量 Read = 1; // “读文件”的互斥锁
信号量 Write = 1; // “写文件”的互斥锁
信号量 ReaderMutex = 1; // 变量 ReaderCount 的互斥锁
信号量 WriterMutex = 1; // 变量 WriterCount 的互斥锁
Reader(){ // 读者进程
while(1){
P(Read); // 每个读进程都需要对 Read 加锁
P(ReaderMutex); // 对 ReadCount 的互斥,实际上,上条语句已经兼有此功能,可以去掉
ReaderCount++;
if (ReaderCount == 1){ // 如果是第一个读进程
P(Write); // 则对写者上锁
}
V(ReaderMutex); // 对 ReadCount 的互斥,实际上,下条语句已经兼有此功能,可以去掉
V(Read); // Read 解锁
读文件;
P(ReaderMutex); // 对 ReadCount 的互斥
ReaderCount--;
if (ReaderCount == 0){ // 如果是最后一个读进程
V(Write); // 则对写者解锁
}
V(ReaderMutex); // 对 ReadCount 的互斥
}
}
Writer(){ // 写者进程
while(1){
P(WriterMutex); // 对 WriterCount 的互斥
WriterCount++;
if (WriterCount == 1){ // 如果是第一个写进程
P(Read); // 则对读者上锁
}
V(WriterMutex); // 对 WriterCount 的互斥
P(Write); // Write 加锁
写文件;
V(Write); // Write 解锁
P(WriterMutex); // 对 WriterCount 的互斥
WriterCount--;
if (WriterCount == 0){ // 如果是最后一个写进程
V(Read); // 则对读者解锁
}
V(WriterMutex); // 对 WriterCount 的互斥
}
}
如果读者写者到达的顺序是:读者 1–读者2–读者 3–写者 A–读者 4–写者 B–读者 5
,则:
【题目 1】有桥如下图所示,车流如箭头所示,桥上不允许两车交汇,但允许同方向多辆车依次通过(即桥上可以有多个同方向的车)。用P、V操作实现交通管理以防止桥上堵塞。
【解答】直接套“三段式”,如下:
int count1 = 0; // 北到南的车辆数
int count2 = 0; // 南到北车辆数
信号量 bridge = 1;
信号量 mutex1 = 1;
信号量 mutex2 = 1;
北到南(){
P(mutex1);
count1++;
if (count1 == 1){
P(bridge);
}
V(mutex1);
北到南过桥;
P(mutex1);
count1--;
if (count1 == 0){
V(bridge);
}
V(mutex1);
}
南到北(){
P(mutex2);
count2++;
if (count2 == 1){
P(bridge);
}
V(mutex2);
南到北过桥;
P(mutex2);
count2--;
if (count2 == 0){
V(bridge);
}
V(mutex2);
}
【题目 2】假设一个录像厅有 0,1,2 三种不同的录像片可由观众选择放映。录像厅的放映规则为:
【解答 1】本题也可以直接套“三段式”,如下:
int count0 = 0; // 看影片 0 的观众数
int count1 = 0; // 看影片 1 的观众数
int count2 = 0; // 看影片 2 的观众数
信号量 movie = 1;
信号量 mutex0 = 1;
信号量 mutex1 = 1;
信号量 mutex2 = 1;
看影片0的观众(){
P(mutex0);
count0++;
if (count0 == 1){
P(movie);
}
V(mutex0);
看影片0;
P(mutex0);
count0--;
if (count0 == 0){
V(movie);
}
V(mutex0);
}
看影片1的观众(){
P(mutex1);
count1++;
if (count1 == 1){
P(movie);
}
V(mutex1);
看影片1;
P(mutex1);
count1--;
if (count1 == 0){
V(movie);
}
V(mutex1);
}
看影片2的观众(){
P(mutex2);
count2++;
if (count2 == 1){
P(movie);
}
V(mutex2);
看影片2;
P(mutex2);
count2--;
if (count2 == 0){
V(movie);
}
V(mutex2);
}
【解答 2】借助“写进程优先”的思想,观看某影片的观众在进去前,可以先把要观看其他影片的观众先上锁,让他们暂时阻塞,如下:
int count0 = 0; // 看影片 0 的观众数
int count1 = 0; // 看影片 1 的观众数
int count2 = 0; // 看影片 2 的观众数
信号量 mutex0 = 1; // 影片 0 的互斥锁,兼有对变量 count0 的互斥功能
信号量 mutex1 = 1; // 影片 1 的互斥锁,兼有对变量 count1 的互斥功能
信号量 mutex2 = 1; // 影片 2 的互斥锁,兼有对变量 count2 的互斥功能
看影片0的观众(){
P(mutex0);
count0++;
if (count0 == 1){ // 第一个进去的观众把其他影片的观众“挡住”
P(mutex1);
P(mutex2);
}
V(mutex0);
看影片0;
P(mutex0);
count0--;
if (count0 == 0){ // 最后一个出来的观众允许其他影片的观众进来
V(mutex1);
V(mutex2);
}
V(mutex0);
}
看影片1的观众(){
P(mutex1);
count1++;
if (count1 == 1){ // 第一个进去的观众把其他影片的观众“挡住”
P(mutex0);
P(mutex2);
}
V(mutex1);
看影片1;
P(mutex1);
count1--;
if (count1 == 0){ // 最后一个出来的观众允许其他影片的观众进来
V(mutex0);
V(mutex2);
}
V(mutex1);
}
看影片2的观众(){
P(mutex2);
count2++;
if (count2 == 1){ // 第一个进去的观众把其他影片的观众“挡住”
P(mutex0);
P(mutex1);
}
V(mutex2);
看影片2;
P(mutex2);
count2--;
if (count2 == 0){ // 最后一个出来的观众允许其他影片的观众进来
V(mutex0);
V(mutex1);
}
V(mutex2);
}
看似没毛病吧。然而,事实上这段代码是有问题的,可能会引发死锁,哈哈哈……你发现了吗?
【题目 3】某男⼦⾜球俱乐部,有教练、队员若⼲。每次⾜球训练开始之前,教练、球员都需要先进⼊更⾐室换⾐服,可惜俱乐部只有⼀个更⾐室。教练们脸⽪薄,⽆法接受和别⼈共⽤更⾐室。队员们脸⽪厚,可以和其他队员⼀起使⽤更⾐室。如果队员和教练都要使⽤更⾐室,则应该让教练优先。请使⽤ P、V 操作描述上述过程的互斥与同步,并说明所⽤信号量及初值的含义。
【解答】把教练视为写进程,把队员视为读进程,更衣室视为缓冲区,则该题需要实现的“写进程优先”。如下:
int ReaderCount = 0; // 读者数量
int WriterCount = 0; // 写者数量
信号量 Read = 1; // “读文件”的互斥锁
信号量 Write = 1; // “写文件”的互斥锁
信号量 ReaderMutex = 1; // 变量 ReaderCount 的互斥锁
信号量 WriterMutex = 1; // 变量 WriterCount 的互斥锁
Reader(){ // 读者进程(相当于教练)
while(1){
P(Read); // 每个读进程都需要对 Read 加锁
P(ReaderMutex); // 对 ReadCount 的互斥
ReaderCount++;
if (ReaderCount == 1){ // 如果是第一个读进程
P(Write); // 则对写者上锁
}
V(ReaderMutex); // 对 ReadCount 的互斥
V(Read); // Read 解锁
读文件;
P(ReaderMutex); // 对 ReadCount 的互斥
ReaderCount--;
if (ReaderCount == 0){ // 如果是最后一个读进程
V(Write); // 则对写者解锁
}
V(ReaderMutex); // 对 ReadCount 的互斥
}
}
Writer(){ // 写者进程(相当于队员)
while(1){
P(WriterMutex); // 对 WriterCount 的互斥
WriterCount++;
if (WriterCount == 1){ // 如果是第一个写进程
P(Read); // 则对读者上锁
}
V(WriterMutex); // 对 WriterCount 的互斥
P(Write); // Write 加锁
写文件;
V(Write); // Write 解锁
P(WriterMutex); // 对 WriterCount 的互斥
WriterCount--;
if (WriterCount == 0){ // 如果是最后一个写进程
V(Read); // 则对读者解锁
}
V(WriterMutex); // 对 WriterCount 的互斥
}
}