Linux学习记录--利用信号量来调度共享资源(2)

一.读者-写者问题

在上一篇文章中,讲了利用信号量来调度共享资源的一种典型案例生产者-消费者问题,本篇讲另一个经典案例,读者-写者问题。

读者-写者问题是互斥问题的一个概括。一组并发的线程要访问一个共享对象,例如一个主存中的数据结构,或者一个磁盘上的数据库。有些线程只读对象,而其他的线程只修改对象。修改对象的线程叫做写者。只读的线程叫做读者。写者必须拥有对对象的独占访问,而读者可以和无限多个其他读者共享对象。

读者-写者问题很常见,比如在手机上选电影票,你可以作为读者与其他读者一起浏览座位,但正在你正在预定一个座位时,此座位便被你占有,其余人士无法选择。

读者-写者问题也存在几个变种,分别基于读者和写者的优先级。下面就这两个优先级分类分别讲讲。

二.读者优先

读者优先,要求不要让读者等待,除非已经把使用对象的权限赋予了一个写者。也就是说,读者不会因为写者在等待排在写者后面。

下面提供一个案例:这里引入了随机数来随即生成读或写,写比较费事,设计写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;
}

其中一个运行结果:

当有写者等待时,后续的读者必须等待写者完成。

优点 :保证写者不会饥饿
缺点:读操作并发度降低

四.总结

策略 优点 缺点 适用场景
读者优先 读并发度高 写者可能饥饿 读多写少
写者优先 写者不会饥饿 读并发度低 写操作重要

进一步优化:可以实现公平的读者-写者算法,避免任何一方饥饿,如使用额外的队列管理等待顺序。

实际应用:数据库系统的并发控制、文件系统的读写锁等都基于读者-写者问题的解决方案。

相关推荐
子牙老师5 分钟前
从零手写gdb调试器
c语言·linux内核·gdb·调试器
Madison-No715 分钟前
【C++】探秘vector的底层实现
java·c++·算法
晚风残17 分钟前
【C++ Primer】第十二章:动态内存管理
开发语言·c++·c++ primer
sulikey23 分钟前
【Linux权限机制深入理解】为何没有目录写权限仍能修改文件权限?
linux·运维·笔记·ubuntu·centos
十安_数学好题速析26 分钟前
倍数关系:最多能选出多少个数
笔记·学习·高考
vue学习30 分钟前
docker 学习dockerfile 构建 Nginx 镜像-部署 nginx 静态网
java·学习·docker
liu****34 分钟前
8.list的模拟实现
linux·数据结构·c++·算法·list
biubiubiu07061 小时前
VPS SSH密钥登录配置指南:告别密码,拥抱安全
linux
保持低旋律节奏1 小时前
C++ stack、queue栈和队列的使用——附加算法题
c++
小莞尔1 小时前
【51单片机】【protues仿真】基于51单片机主从串行通信系统
c语言·单片机·嵌入式硬件·物联网·51单片机