一.读者-写者问题
在上一篇文章中,讲了利用信号量来调度共享资源的一种典型案例生产者-消费者问题,本篇讲另一个经典案例,读者-写者问题。
读者-写者问题是互斥问题的一个概括。一组并发的线程要访问一个共享对象,例如一个主存中的数据结构,或者一个磁盘上的数据库。有些线程只读对象,而其他的线程只修改对象。修改对象的线程叫做写者。只读的线程叫做读者。写者必须拥有对对象的独占访问,而读者可以和无限多个其他读者共享对象。
读者-写者问题很常见,比如在手机上选电影票,你可以作为读者与其他读者一起浏览座位,但正在你正在预定一个座位时,此座位便被你占有,其余人士无法选择。
读者-写者问题也存在几个变种,分别基于读者和写者的优先级。下面就这两个优先级分类分别讲讲。
二.读者优先
读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者。也就是说,读者不会因为写者在等待排在写者后面。
下面提供一个案例:这里引入了随机数来随即生成读或写,写比较费事,设计写2/5,读3/5,他们的消耗时间也是随机生成,不过为了感受写费时,在写额外增加时间,下面同理。
cpp
#include "c_pthread_box.h"
#include <time.h>
#include <string.h>
#include <stdint.h>
int msleep(long msec) /* 自己包一层,返回 0 成功,-1 失败 */
{
struct timespec ts;
ts.tv_sec = msec / 1000;
ts.tv_nsec = (msec % 1000) * 1000000L;
return nanosleep(&ts, NULL); /* 精确到纳秒,可被打断 */
}
/* Global variables */
int readcnt = 0; /* The number of reader ,initally = 0 */
sem_t mutex,w; /* This mutex protects reading (readcnt), w protect write, both initiall = 1 */
int a = 10, b = 99;
void* reader(void* vargp)
{
int myid = (int)(intptr_t)vargp;
static int rcnt = 0 ;
int rid ;
P(&mutex);
readcnt++;
rcnt++;
rid = rcnt;
printf("[%d],reader%d: 在读\n",myid,rid);
if(readcnt == 1){ /* 如果是第一位读者,就先对写者添加限制,保障读者 */
P(&w);
}
V(&mutex); /* 因为读者优先,因此没到最后一位读者读完,不会解开写者的锁 */
/* Critical section */
/* Reading happens */
int in_range = a + rand() % (b - a + 1);
msleep(in_range);
P(&mutex);
readcnt--;
if(readcnt == 0){ /* 如果是最后一位读者,退出前解除对写者的限制,让写者可去操作 */
V(&w);
}
printf("[%d],reader%d: 读完\n",myid,rid);
V(&mutex);
}
void* writer(void* vargp)
{
int myid = (int)(intptr_t)vargp;
static int wcnt = 0;
wcnt++;
int wid = wcnt ;
printf("[%d],writer%d: 想要写\n",myid,wid);
P(&w);
printf("[%d],writer%d: 在写\n",myid,wid);
/* Critical section */
/* Writing happens */
int in_range = a + 100 + rand() % (b - a + 1);
msleep(in_range);
printf("[%d],writer%d: 写完\n",myid,wid);
V(&w);
}
int main(int argc,char** argv)
{
int N , i ,w_r;
// pthread_t rtid,wtid;
if(argc != 2){
fprintf(stderr, "usage: %s <N>\n", argv[0]);
exit(-1);
}
srand((unsigned)time(NULL)); /* 播种 */
N = atoi(argv[1]);
pthread_t *tids = malloc(N * sizeof(pthread_t));
sem_init(&mutex,0,1);
sem_init(&w,0,1);
for(i = 0; i < N;i++ ){
w_r = rand() % (5);
if(w_r <= 1){
pthread_create(&tids[i],NULL,writer,(void*)(intptr_t)i);
}
else{
pthread_create(&tids[i],NULL,reader,(void*)(intptr_t)i);
}
}
for(i = 0 ;i < N; i++){
pthread_join(tids[i],NULL);
}
return 0;
}
其中一次运行结果:

当有读者正在读时,后续的读者可以直接加入,写者必须等待所有读者完成。
优点 :读操作并发度高
缺点:写者可能饥饿(长时间等待)
Tips:
饥饿 (starvation):饥饿就是一个线程无限期地阻塞,无法进展。例如本例,写者一直想要写,但最后也是一直等待,直到读者全部读完才能去写。
三.写者优先
与读者优先相反,读者更有优先级,不会因写者等待而等待。
cpp
#include "c_pthread_box.h"
#include <time.h>
#include <string.h>
#include <stdint.h>
int msleep(long msec) /* 自己包一层,返回 0 成功,-1 失败 */
{
struct timespec ts;
ts.tv_sec = msec / 1000;
ts.tv_nsec = (msec % 1000) * 1000000L;
return nanosleep(&ts, NULL); /* 精确到纳秒,可被打断 */
}
/* Global variables */
int writercnt = 0; /* The number of writerer ,initally = 0 */
sem_t mutex,r; /* This mutex protects writeing (writecnt), r protect read, both initiall = 1 */
/* 3. 指定范围 [a,b] */
int a = 10, b = 99;
void* writer(void* vargp)
{
int myid = (int)(intptr_t)vargp;
static int wcnt = 0 ;
int wid ;
P(&mutex);
writercnt++;
wcnt++;
wid = wcnt;
printf("[%d],writerer%d: 在写\n",myid,wid);
if(writercnt == 1){ /* 如果第一位写者,就先对读者添加限制,保障写者 */
P(&r);
}
V(&mutex); /* 因为写者优先,因此没到最后一位写者写完,不会解开读者的锁 */
/* Critical section */
/* Reading happens */
int in_range = a + rand() % (b - a + 1);
msleep(in_range);
P(&mutex);
writercnt--;
if(writercnt == 0){ /* 如果是最后一位写者,退出前解除对读者的限制,让读者可去操作 */
V(&r);
}
printf("[%d],writerer%d: 写完\n",myid,wid);
V(&mutex);
}
void* reader(void* vargp)
{
int myid = (int)(intptr_t)vargp;
static int rcnt = 0;
rcnt++;
int rid = rcnt ;
printf("[%d],reader%d: 想要读\n",myid,rid);
P(&r);
printf("[%d],reader%d: 在读\n",myid,rid);
/* Critical section */
/* Writing happens */
int in_range = a + 100 + rand() % (b - a + 1);
msleep(in_range);
printf("[%d],reader%d: 读完\n",myid,rid);
V(&r);
}
int main(int argc,char** argv)
{
int N , i ,w_r;
// pthread_t rtid,wtid;
if(argc != 2){
fprintf(stderr, "usage: %s <N>\n", argv[0]);
exit(-1);
}
srand((unsigned)time(NULL)); /* 播种 */
N = atoi(argv[1]);
pthread_t *tids = malloc(N * sizeof(pthread_t));
sem_init(&mutex,0,1);
sem_init(&r,0,1);
for(i = 0; i < N;i++ ){
w_r = rand() % (5);
if(w_r > 1){
pthread_create(&tids[i],NULL,writer,(void*)(intptr_t)i);
}
else{
pthread_create(&tids[i],NULL,reader,(void*)(intptr_t)i);
}
}
for(i = 0 ;i < N; i++){
pthread_join(tids[i],NULL);
}
return 0;
}
其中一个运行结果:

当有写者等待时,后续的读者必须等待写者完成。
优点 :保证写者不会饥饿
缺点:读操作并发度降低
四.总结
策略 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
读者优先 | 读并发度高 | 写者可能饥饿 | 读多写少 |
写者优先 | 写者不会饥饿 | 读并发度低 | 写操作重要 |
进一步优化:可以实现公平的读者-写者算法,避免任何一方饥饿,如使用额外的队列管理等待顺序。
实际应用:数据库系统的并发控制、文件系统的读写锁等都基于读者-写者问题的解决方案。