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;
}

其中一个运行结果:

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

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

四.总结

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

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

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

相关推荐
biubiubiu07062 小时前
coqui-ai/TTS 安装
linux·运维·服务器
1白天的黑夜12 小时前
队列+宽搜(BFS)-662.二叉树最大宽度-力扣(LeetCode)
c++·leetcode·宽度优先·队列
yihai-lin3 小时前
Rust/C/C++ 混合构建 - Cmake集成Cargo编译动态库
c语言·c++·rust
10001hours3 小时前
C语言第20讲
c语言·开发语言
打不了嗝 ᥬ᭄3 小时前
【Linux】UDP 网络编程
linux·运维·服务器
kcoo3 小时前
Jupyter Lab 汉化
linux·开发语言·python
m0_552200823 小时前
《UE5_C++多人TPS完整教程》学习笔记59 ——《P60 投射物武器(Projectile Weapons)》
c++·游戏·ue5
Lynnxiaowen3 小时前
今天我们开始学习nginx缓存功能,CORS以及nginx防盗链
linux·运维·学习·nginx·云计算·bash
无挂写代码3 小时前
Linux开发工具(编辑器gcc/g++,make/Makefile,gdb)
linux·运维·服务器