史上最准确清晰的读者写者问题介绍,有任何不懂的问题的同学留言区留言。
读者写者问题是并发程序设计中的经典问题。问题描述为:对于同一个文件,读操作可以同时并行,读写操作互斥,写与写互斥。看教科书和PPT的时候发现代码都有问题,于是上维基百科搜了下,解释很到位,代码很漂亮。下面是笔记和个人的些许解读。
第一个读者写者问题:读者优先,写者可能饿死
动机:所有读者都能够同时读文件
semaphore resource=1;
semaphore rmutex=1;
readcount=0;
/*
resource.P() 与 wait(resource) 等价
resource.V() 与 signal(resource) 等价
rmutex.P() 与 wait(rmutex) 等价
rmutex.V() 与 signal(rmutex) 等价
*/
writer() {
resource.P(); //文件上锁
writeFile();
resource.V(); //开锁允许其他进程的读或写操作
}
reader() {
rmutex.P(); //上锁进行读者计数更新,防止多个读者计数发生计数冲突的问题
readcount++; //读者计数加一
if (readcount == 1) //检查是否为第一个读者
resource.P(); //如果是,则对文件上锁
rmutex.V(); //开锁允许其他读者
readFile();
rmutex.P(); //上锁进行读者计数更新,防止多个读者计数发生计数冲突的问题
readcount--; //因为已经完成读操作,所以读者计数减一
if (readcount == 0) //检查自己是否为最后一个读者
resource.V(); //如果是,则释放文件资源,之后写者可以进行写操作
rmutex.V(); //开锁允许其他读者
}
在上面的 reader() 程序中,设置了条件 readcount==1,而不是设置为 readcount>0。这是因为,一个读者和多个读者占用的都只有一个文件,不需要对 resource 进行多次的占用和释放,只需要第一个读者对文件执行占用,最后一个读者对文件执行释放即可。如果设置为 readcount > 0,上面的程序的其他地方不修改,则是有问题的,将不能够实现多个读者同时读文件,只能一个读者读文件。也正是由于最后一个读者才会释放文件资源,所以当有写者的时候,写者会被阻塞直到所有的读者完成操作。因此这种解法的问题就是可能会饿死写者,该解法因此是读者优先的。
第二个读者写者问题:写者优先,读者可能饿死
动机:写者应该尽早开始写操作
int readcount=0, writecount=0;
semaphore rmutex=1, wmutex=1, readTry=1, resource=1;
reader() {
readTry.P(); //表示一个读者尝试进入
rmutex.P(); //上锁进行读者计数更新,防止多个读者计数发生计数冲突的问题
readcount++; //更新读者计数器
if (readcount == 1) //检查自己是否为第一个读者
resource.P(); //如果是,则对文件上锁
rmutex.V(); //开锁允许其他读者
readTry.V(); //表示已经进入并且退出
readFile();
rmutex.P(); //上锁进行读者计数更新,防止多个读者计数发生计数冲突的问题
readcount--; //因为已经完成读操作,所以读者计数减一
if (readcount == 0) //检查自己是否为最后一个读者
resource.V(); //如果是,则释放文件资源
rmutex.V(); //开锁允许其他读者
}
writer() {
wmutex.P(); //上锁进行写者计数更新,防止多个写者计数发生计数冲突的问题
writecount++; //更新写者计数器
if (writecount == 1) //检查自己是否为第一个写者
readTry.P(); //如果是,则抢 readTry 的锁,从而避免饿死
wmutex.V(); //开锁允许其他写者
resource.P(); //对资源上锁,防止其他写者同时进行写操作
writeFile();
resource.V(); //释放资源
wmutex.P(); //上锁进行写者计数更新,防止多个写者计数发生计数冲突的问题
writecount--; //更新写者计数器
if (writecount == 0) //检查自己是否为最后一个写者
readTry.V(); //如果是,则放开 readTry 的锁,让读者可以进入
wmutex.V(); //开锁
}
在上面的程序中,通过 readTry 来实现写者优先,resource 是资源锁,readTry 相当于大门锁。你只有进得了大门,才能拿得了资源,所以我觉得 readTry 改名为 door 更贴切。读者计数结束之后,就把门锁放在一边,这个时候如果写者来了,那么写者就会把门锁拾起来,等读者读操作结束释放资源后,写者就可以开始写了。同样地,由于是最后一个写者释放门锁,所以,只有当所有的写者完成操作后,读者才能进入,所以读者可能饿死。门锁是关键啊!
第三个读者写者问题:
动机:不允许读者或写者饿死
int readcount=0;
// 初始值全为1
semaphore resource=1;
semaphore rmutex=1;
semaphore serviceQueue=1; // 请求按照先进先出的顺序进入队列
reader() {
serviceQueue.P();
// 下面的rmutex.P()和与之匹配的rmutex.V()虽然可以省略
// 但是这样做之后缺乏可读性,所以还是不要删掉的好
rmutex.P();
readcount++;
if (readcount == 1)
resource.P();
serviceQueue.V();
rmutex.V();
readFile();
rmutex.P();
readcount--;
if (readcount == 0)
resource.V();
rmutex.V();
}
writer() {
serviceQueue.P();
resource.P();
serviceQueue.V();
writeFile();
resource.V();
}
上面的代码的 reader() 部分其实和第二个读者写者问题的 reader() 是一样的,不一样的是写者。这里的 serviceQueue 也可以看做是门锁。不过这里并不会出现写者优先,而是按照先进先出的顺序让读者写者使用资源。
serviceQueue 一次对外面提供一个服务的空位,不管是读者还是写者,serviceQueue 的作用是让它们对 resource 进行占用,一旦占用成功,则 serviceQueue 的空位就腾出来了。上面之所以删除那两行,是因为 serviceQueue 在当前的语境下的作用已经包含对 readcount 操作互斥的效果了,所以不需要 rmutex。
计算机世界有时候想想也挺有意思的,在这个世界中我们经常会去寻找接近绝对公平的算法来解决一个问题,也许这就是计算机世界的蜜汁迷人之处吧~
参考文档:Reader-Writers Problem