linux学习进程 线程同步——读写锁

在Linux线程编程中,我们已经学习了互斥锁(mutex),它能有效解决线程间的竞争问题,但互斥锁存在一个局限性:无论线程是读取资源还是修改资源,都会独占锁,导致读取操作之间也会相互阻塞,降低程序的并发效率。而读写锁(Read-Write Lock)正是为了解决这一问题而生,它区分了"读取操作"和"写入操作",实现了"读共享、写独占"的机制,大幅提升了读多写少场景下的程序性能。

一、读写锁的核心概念

读写锁,也叫共享-独占锁(Shared-Exclusive Lock),核心逻辑是:

多个线程可以同时持有读锁(共享锁),此时线程只能对资源执行读取操作,不能修改;

只有一个线程可以持有写锁(独占锁),此时其他线程(无论是读还是写)都无法获取任何锁,只能阻塞等待;

读锁和写锁不能同时存在,写锁优先级通常高于读锁(避免写操作长期被读操作阻塞,不同系统可能有差异)。

简单来说:读可以共享,写必须独占。这一特性使其特别适合"读多写少"的场景,比如日志读取、配置文件读取、数据库查询等,既能保证数据一致性,又能最大化并发效率。

二、读写锁与互斥锁的区别

为了更清晰理解读写锁的优势,我们对比一下它与互斥锁的核心差异:

特性 互斥锁(mutex) 读写锁(rwlock)
锁类型 单一独占锁 读锁(共享)、写锁(独占)
并发能力 低,同一时刻只能有一个线程持有锁 高,读操作可并发,仅写操作独占
适用场景 读少写多、读写频率相近 读多写少
阻塞情况 读-读、读-写、写-写均阻塞 读-读不阻塞,读-写、写-写阻塞

三、读写锁的常用API(Linux系统)

Linux中读写锁的相关操作都定义在 <pthread.h> 头文件中,核心API分为4类:初始化、加锁、解锁、销毁。需要注意的是,读写锁的变量类型为 pthread_rwlock_t

1. 初始化读写锁

有两种初始化方式:静态初始化和动态初始化。

cpp 复制代码
// 1. 静态初始化(推荐,简单高效)
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

// 2. 动态初始化(需手动销毁)
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

参数说明:

rwlock:指向读写锁变量的指针;

attr:读写锁的属性,通常设为NULL(使用默认属性);

返回值:成功返回0,失败返回非0错误码。

2. 加锁操作(核心)

读写锁有两种加锁方式,分别对应读操作和写操作,还有非阻塞版本(避免线程长期阻塞)。

cpp 复制代码
// 加读锁(共享锁)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

// 加写锁(独占锁)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

// 非阻塞加读锁(尝试加锁,失败立即返回,不阻塞)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

// 非阻塞加写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

说明:

阻塞版本(rdlock/wrlock):如果锁被占用,线程会阻塞,直到获取到锁;

非阻塞版本(tryrdlock/trywrlock):如果锁被占用,不会阻塞,直接返回错误码(EBUSY),适合不需要等待的场景;

返回值:成功返回0,失败返回非0错误码。

3. 解锁操作

无论加的是读锁还是写锁,都使用同一个解锁函数,系统会自动区分锁类型。

cpp 复制代码
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

注意:必须由持有锁的线程解锁,否则会导致未定义行为(比如程序崩溃)。

4. 销毁读写锁

仅动态初始化的读写锁需要手动销毁,静态初始化的无需销毁(系统自动回收)。

cpp 复制代码
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

说明:销毁前,必须确保所有线程都已释放锁,否则会返回错误。

四、实战案例:用读写锁实现"读多写少"场景

我们通过一个简单的案例,演示读写锁的使用:假设有一个共享变量(模拟数据),多个读线程读取该变量,1个写线程修改该变量,用读写锁保证数据一致性,同时提升读操作的并发效率。

案例代码

cpp 复制代码
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// 共享变量(模拟数据)
int shared_data = 100;
// 读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

// 读线程函数:读取共享变量
void *read_thread(void *arg) {
    int tid = *(int *)arg;
    while (1) {
        // 加读锁
        pthread_rwlock_rdlock(&rwlock);
        printf("读线程%d:读取到数据 = %d\n", tid, shared_data);
        // 解锁
        pthread_rwlock_unlock(&rwlock);
        // 模拟读操作耗时
        sleep(1);
    }
    return NULL;
}

// 写线程函数:修改共享变量
void *write_thread(void *arg) {
    int tid = *(int *)arg;
    while (1) {
        // 加写锁
        pthread_rwlock_wrlock(&rwlock);
        // 修改共享变量
        shared_data++;
        printf("=== 写线程%d:修改数据为 = %d ===\n", tid, shared_data);
        // 解锁
        pthread_rwlock_unlock(&rwlock);
        // 模拟写操作耗时(写频率低于读)
        sleep(3);
    }
    return NULL;
}

int main() {
    pthread_t read_tids[3], write_tid;
    int tids[3] = {1, 2, 3};
    int i;

    // 创建3个读线程
    for (i = 0; i < 3; i++) {
        pthread_create(&read_tids[i], NULL, read_thread, &tids[i]);
    }

    // 创建1个写线程
    pthread_create(&write_tid, NULL, write_thread, &i);

    // 等待线程结束(实际中不会退出,这里仅作演示)
    for (i = 0; i < 3; i++) {
        pthread_join(read_tids[i], NULL);
    }
    pthread_join(write_tid, NULL);

    // 销毁读写锁(静态初始化可省略,但写了也没错)
    pthread_rwlock_destroy(&rwlock);
    return 0;
}

编译与运行

编译时需要链接 pthread 库(Linux下线程库默认不自动链接):

bash 复制代码
gcc rwlock_demo.c -o rwlock_demo -lpthread
./rwlock_demo

运行结果分析

从运行结果中可以观察到两个关键现象:

  1. 3个读线程可以同时读取数据(输出连续的"读线程"信息),不会相互阻塞;

  2. 当写线程开始修改数据时,所有读线程都会阻塞,直到写线程解锁,之后读线程才能继续读取(写操作独占锁);

  3. 写操作频率低于读操作,充分体现了读写锁"读共享"的优势,提升了并发效率。

五、读写锁的注意事项(避坑重点)

  1. 锁的顺序问题:同一线程中,不能先加写锁再加读锁(会导致死锁);可以先加读锁,再尝试加写锁,但此时会阻塞(因为读锁和写锁不能共存)。

  2. 写锁优先级:Linux默认写锁优先级高于读锁,即当有写线程等待时,新的读线程会被阻塞,优先让写线程获取锁,避免写操作"饥饿"(长期得不到执行)。

  3. 非阻塞锁的使用:非阻塞版本(tryrdlock/trywrlock)返回EBUSY时,不要直接死循环重试,建议适当延时后再尝试,避免占用过多CPU资源。

  4. 资源释放:动态初始化的读写锁,必须在所有线程结束后销毁;如果线程异常退出,要确保锁被释放,否则会导致其他线程永久阻塞。

  5. 适用场景:读写锁只适合"读多写少"的场景,如果写操作频繁,使用读写锁反而会因为锁切换带来额外开销,此时不如使用互斥锁更高效。

六、总结

读写锁是Linux线程同步中一种高效的同步机制,核心是"读共享、写独占",解决了互斥锁在"读多写少"场景下并发效率低的问题。

核心要点回顾:

读写锁分为读锁(共享)和写锁(独占),读-读不阻塞,读-写、写-写阻塞;

常用API:初始化(init)、加读锁(rdlock)、加写锁(wrlock)、解锁(unlock)、销毁(destroy);

适合读多写少场景,写频繁场景建议用互斥锁;

注意避免死锁、确保锁的正确释放,关注写锁优先级问题。

下一节,我们将学习线程同步的其他机制(如条件变量),进一步完善线程同步的知识体系。

相关推荐
sinat_383437361 小时前
如何实现SQL简单数据的映射查询_使用CASE表达式替换
jvm·数据库·python
ZWZhangYu1 小时前
MCP 实战:从协议原理到 Java 自定义工具服务落地
java·开发语言·人工智能
2401_835956811 小时前
JavaScript 中实现基于分组的前端产品筛选功能
jvm·数据库·python
Flittly1 小时前
【SpringSecurity新手村系列】(5)RBAC角色权限与账户状态校验
java·spring boot·笔记·安全·spring·ai
笨蛋不要掉眼泪1 小时前
面试篇-java基础下
java·后端·面试·职场和发展
知识分享小能手2 小时前
R语言入门学习教程,从入门到精通,R语言基础 - 完整知识点与案例代码(1)
开发语言·学习·r语言
m0_746752302 小时前
SQL中窗口函数的LIMIT限制逻辑_如何分页显示
jvm·数据库·python
wechatbot8882 小时前
企业微信 iPad 协议客服机器人自动化管理平台开发指南
java·运维·微信·自动化·企业微信·ipad
m0_514520572 小时前
Go语言怎么做自动补全_Go语言CLI自动补全教程【经典】
jvm·数据库·python