用 Linux 提供的读写锁实现读者写者模型

什么是读者写者模型?

要认识读者写者模型,我们就从一二三原则出发,通过读者写者模型中的一二三来认识他

  1. 一指的是读者和写者共享同一个缓冲区(临界资源)
  2. 二指的是两个角色。一个角色是读者,用来读取缓冲区中的内容。,一个角色是写者,用来向缓冲区里面写内容。
  3. 三指的是三个关系:
    3.1 读者与读者之间的关系:允许不同读者同时访问临界区
    3.2 读者与写者之间的关系:读者与写者对临界区的访问是互斥的。读者必须等到学者写完再读,写者也必须等到读者读完再写。
    3.3 写者与写者之间的关系:不同写者之间访问临界区是互斥的。即同一个时刻只能有一个写者在缓冲区中写。

实现读者写者模型中的进程同步互斥关系

在真正实现读者写者模型的时候,也两种实现策略,一种是读优先,另一种是写优先。下面我们就以读优先为例,来看一看用信号量机制如何实现读者写者模型(用的是王道咸鱼老师的原码)
首先可以看到我们在这个代码中定义了两把锁。还定义了一个count变量用来计数。第一把锁rw用来实现读写进程对缓冲区的互斥访问。第二把锁mutex就用来实现计数操作的原子性。
这个计数器count是用来干啥的?这就牵扯到我们读优先的读写模型中一个重要的同步关系:那就是只有所有的读者都读完了,写进程才能写。大家想想这个同步关系应该怎么实现呢?我们的解决方法是引入一个计数器,用来实时记录访问共享文件的读进程的数量。在第一个读进程进入临界区之前,我们就把rw这把锁给锁上,在最后一个读进程离开临界区之后,我们才把rw这把锁给解开。
理解了这些全局变量的功能,那么我们的模型也就很好实现了

int count=0;                //用于记录当前的读者数量
semaphore mutex=1;          //用于保护更新count变量时的互斥
semaphore rw=1;             //用于保证读者和写者互斥地访问文件
writer(){                   //写者进程
    while(1){
        P(rw);              //互斥访问共享文件
        writing;            //写入
        V(rw);              //释放共享文件
    }
}
reader(){                   //读者进程
    while(1){
        P(mutex);           //互斥访问count变量
        if(count==0)
            P(rw);          //阻止写进程写
        count++;            //读者计数器加1
        V(mutex);           //释放互斥变量count
        reading;            //读取
        P(mutex);           //互斥访问count变量
        count--;            //读者计数器减1
        if(count==0)
            V(rw);          //允许写进程写
        V(mutex);           //释放互斥变量count
    }
}

在Linux系统中,为了方便用户更简单的去使用读者写者模型,开发者在pthread 库中定义了一把读写锁,引入这把锁之后,我们的读进程和写进程只需要在访问临界区之前执行P操作。访问临界区之后执行V操作。就可以实现我们上述代码中描述的读者写者模型中的同步互斥关系。

Linux 中读写锁的相关调用接口简单介绍

在Linux系统中,读写锁相关操作依赖于POSIX线程库(pthread库),头文件为<pthread.h> ,主要的调用接口有下面四个:

  1. pthread_rwlock_init():用来进行读写锁的初始化

  2. pthread_rwlock_destroy:销毁一把读写锁

  3. pthread_rwlock_rdlock():用来给读进程上读锁
    上这把锁之前,读进程会去检查写锁有没有被上,但凡写锁已经被上了,说明这个时候有写进程在访问临界区资源,此时读进程就会陷入阻塞,而如果写锁没有被上,读进程就可以成功上一把读锁,这把锁一旦上了之后,其余的任何进程在调用pthread_rwlock_wrlock的时候都会被阻塞,而其余任何进程在调用pthread_rwlock_rdlock()时却不会收到影响,该上还是上

  4. pthread_rwlock_wrlock:用来给写进程上写锁(上这把锁之前写进程会去检查写锁和读锁是不是已经被其他进程上过了,只要这两把锁中的任意一把正在被其他线程持有,那么写进程就会陷入阻塞,如果这两把锁在写进程检查的时候都没有被上,那么随后写进程就会上一把写锁,这把锁一旦上了之后,其余的任何进程在调用pthread_rwlock_wrlock()和pthread_rwlock_rdlock()的时候都会陷入阻塞

用这些接口实现一个简单的读者写者模型

使用这些接口时,需要注意编译时要链接pthread库(通过-lpthread选项),并且要正确处理函数返回的错误码,以确保程序的健壮性。
下面是使用读写锁实现读者写者模型的示例。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>

// 共享数据
int shared_data = 0;

// 读写锁
pthread_rwlock_t rwlock;

// 读者线程函数
void *reader(void *arg) {
    int id = *(int *)arg;
    free(arg);  // 释放动态分配的内存
    
    while (1) {
        // 获取读锁
        if (pthread_rwlock_rdlock(&rwlock) != 0) {
            perror("pthread_rwlock_rdlock");
            pthread_exit(NULL);
        }
        
        // 读取共享数据
        printf("读者 %d: 读取到数据 %d\n", id, shared_data);
        
        // 释放读锁
        if (pthread_rwlock_unlock(&rwlock) != 0) {
            perror("pthread_rwlock_unlock");
            pthread_exit(NULL);
        }
        
        // 随机休眠一段时间,模拟处理数据
        sleep(rand() % 3 + 1);
    }
    
    pthread_exit(NULL);
}

// 写者线程函数
void *writer(void *arg) {
    int id = *(int *)arg;
    free(arg);  // 释放动态分配的内存
    
    while (1) {
        // 获取写锁
        if (pthread_rwlock_wrlock(&rwlock) != 0) {
            perror("pthread_rwlock_wrlock");
            pthread_exit(NULL);
        }
        
        // 修改共享数据
        shared_data++;
        printf("===== 写者 %d: 将数据修改为 %d =====\n", id, shared_data);
        
        // 释放写锁
        if (pthread_rwlock_unlock(&rwlock) != 0) {
            perror("pthread_rwlock_unlock");
            pthread_exit(NULL);
        }
        
        // 随机休眠一段时间,模拟处理数据
        sleep(rand() % 5 + 2);
    }
    
    pthread_exit(NULL);
}

int main() {
    pthread_t readers[3], writers[2];
    int i;
    
    // 初始化随机数生成器
    srand(time(NULL));
    
    // 初始化读写锁
    if (pthread_rwlock_init(&rwlock, NULL) != 0) {
        perror("pthread_rwlock_init");
        return 1;
    }
    
    // 创建读者线程
    for (i = 0; i < 3; i++) {
        int *id = malloc(sizeof(int));
        *id = i + 1;
        if (pthread_create(&readers[i], NULL, reader, id) != 0) {
            perror("pthread_create");
            return 1;
        }
    }
    
    // 创建写者线程
    for (i = 0; i < 2; i++) {
        int *id = malloc(sizeof(int));
        *id = i + 1;
        if (pthread_create(&writers[i], NULL, writer, id) != 0) {
            perror("pthread_create");
            return 1;
        }
    }
    
    // 等待所有线程结束(实际上这些线程会无限循环)
    for (i = 0; i < 3; i++) {
        pthread_join(readers[i], NULL);
    }
    for (i = 0; i < 2; i++) {
        pthread_join(writers[i], NULL);
    }
    
    // 销毁读写锁
    pthread_rwlock_destroy(&rwlock);
    
    return 0;
}

linux Pthread库 读写锁中的优先策略

现在我有一个问题。假如说有一个写进程正在共享区中写。他写的过程中有一个读进城调用pthread_rwlock_wrlock()想要读,有另一个写进程调用pthread_rwlock_rdlock()想要写,他们都会因为当前进程正在临界区中写而阻塞。现在当前写进程写完了,他要退出临界区,释放他手里持有的那把写锁,请问释放之后哪个进程会先进入临界区?

  • 在 Linux 等多数系统的默认实现中,读写锁通常采用 “写者优先” 策略:当持有写锁的进程释放锁后,若等待队列中既有写进程在等待,会优先唤醒写进程获取写锁,而不是唤醒所有等待的读进程。
    这样做主要为了避免 “写饥饿” 问题 —— 如果始终让读者优先,持续到来的读进程可能会让写进程长期处于等待状态,无法获得锁。
  • 当然读写锁也支持手动的将优先策略改成读优先,比如在读写锁初始化之后(假设读写锁的名称是rwlock_attr),我们可以调用pthread_rwlockattr_setkind_np(&rwlock_attr, PTHREAD_RWLOCK_PREFER_READER_NP)将策略手动改成读优先

附:Linux 中读写锁的相关调用接口详细介绍

1. 读写锁类型定义

读写锁的类型名称是pthread_rwlock_t ,我们可以用它来定义一把读写锁

pthread_rwlock_t rwlock;

2. 初始化读写锁

函数原型

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

参数说明

  • rwlock:指向要初始化的读写锁变量的指针。
  • attr:指向读写锁属性对象的指针,若为NULL,则使用默认属性。

返回值:成功时返回0,失败时返回一个非零错误码,常见的错误包括ENOMEM(内存不足)等。

示例

pthread_rwlock_t rwlock;
int ret = pthread_rwlock_init(&rwlock, NULL);
if (ret != 0) {
    perror("pthread_rwlock_init");
    // 处理错误
}

3. 销毁读写锁

函数原型

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

参数说明rwlock 是指向要销毁的读写锁变量的指针。

返回值:成功时返回0,失败时返回非零错误码,例如当读写锁仍被锁定时尝试销毁,会返回EBUSY

示例

int ret = pthread_rwlock_destroy(&rwlock);
if (ret != 0) {
    perror("pthread_rwlock_destroy");
    // 处理错误
}

4. 以读模式锁定读写锁

函数原型

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

参数说明rwlock 是指向要获取读锁的读写锁变量的指针。

返回值:成功时返回0,失败时返回非零错误码,如EAGAIN(达到系统资源限制)、EDEADLK(检测到死锁) 等。

说明:当有写者正在持有写锁或者有写者在等待写锁时,调用该函数的读者线程会被阻塞。当没有写者时,多个读者可以同时持有读锁,实现并发读。

示例

int ret = pthread_rwlock_rdlock(&rwlock);
if (ret != 0) {
    perror("pthread_rwlock_rdlock");
    // 处理错误
}
// 读取共享资源的操作
// 完成读取后解锁
pthread_rwlock_unlock(&rwlock); 

5. 以写模式锁定读写锁

函数原型

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

参数说明rwlock 是指向要获取写锁的读写锁变量的指针。

返回值:成功时返回0,失败时返回非零错误码,错误情况和读锁类似。

说明:一旦写者获取了写锁,其他读者线程和写者线程都将被阻塞,直到写者释放写锁。

示例

int ret = pthread_rwlock_wrlock(&rwlock);
if (ret != 0) {
    perror("pthread_rwlock_wrlock");
    // 处理错误
}
// 修改共享资源的操作
// 完成修改后解锁
pthread_rwlock_unlock(&rwlock); 

6. 解锁读写锁

函数原型

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

参数说明rwlock 是指向要解锁的读写锁变量的指针。

返回值:成功时返回0,失败时返回非零错误码,比如当解锁一个未锁定的读写锁时,会返回EPERM

示例

// 读操作或写操作完成后
int ret = pthread_rwlock_unlock(&rwlock);
if (ret != 0) {
    perror("pthread_rwlock_unlock");
    // 处理错误
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值