Linux学习记录--利用信号量来调度共享资源(2)
一.读者-写者问题
在上一篇文章中,讲了利用信号量来调度共享资源的一种典型案例生产者-消费者问题,本篇讲另一个经典案例,读者-写者问题。
读者-写者问题是互斥问题的一个概括。一组并发的线程要访问一个共享对象,例如一个主存中的数据结构,或者一个磁盘上的数据库。有些线程只读对象,而其他的线程只修改对象。修改对象的线程叫做写者。只读的线程叫做读者。写者必须拥有对对象的独占访问,而读者可以和无限多个其他读者共享对象。
读者-写者问题很常见,比如在手机上选电影票,你可以作为读者与其他读者一起浏览座位,但正在你正在预定一个座位时,此座位便被你占有,其余人士无法选择。
读者-写者问题也存在几个变种,分别基于读者和写者的优先级。下面就这两个优先级分类分别讲讲。
二.读者优先
读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者。也就是说,读者不会因为写者在等待排在写者后面。
下面提供一个案例:这里引入了随机数来随即生成读或写,写比较费事,设计写2/5,读3/5,他们的消耗时间也是随机生成,不过为了感受写费时,在写额外增加时间,下面同理。
#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):饥饿就是一个线程无限期地阻塞,无法进展。例如本例,写者一直想要写,但最后也是一直等待,直到读者全部读完才能去写。
三.写者优先
与读者优先相反,读者更有优先级,不会因写者等待而等待。
#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;
}
其中一个运行结果:
当有写者等待时,后续的读者必须等待写者完成。
优点:保证写者不会饥饿
缺点:读操作并发度降低
四.总结
策略 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
读者优先 | 读并发度高 | 写者可能饥饿 | 读多写少 |
写者优先 | 写者不会饥饿 | 读并发度低 | 写操作重要 |
进一步优化:可以实现公平的读者-写者算法,避免任何一方饥饿,如使用额外的队列管理等待顺序。
实际应用:数据库系统的并发控制、文件系统的读写锁等都基于读者-写者问题的解决方案。