读写锁的使用(读写者模型)

本文介绍了读写锁的工作原理及其与互斥锁的区别。通过对比读者和写者在不同场景下的行为,阐述了读写锁如何有效提高并发效率,并提供了一些核心函数及简单的使用示例。

读写锁是互斥锁的一种,两者的区别在于:

  • 互斥锁:线程之间相互排斥,每次只能有一个线程访问临界资源
  • 读写锁
    • 写者(线程)相互排斥,每次只能有一个线程访问临界资源
    • 读者(线程)互不影响,可以同时访问临界资源。

在读取比修改频繁的场景下,读写锁要更合适一点。因为如果使用互斥锁,在读取数据的时候,读线程之间会争抢锁,但是使用读写锁时,读线程之间互不影响。

画黑板报就是一个典型的读写者模型,画板报的人只有一个(即写者),但是读黑板报的人有多个(即读者)


目录

一、模型中的三种关系

1、写者和写者

2、读者和写者

3、读者和读者

二、读写锁加锁/解锁的基本原理

1、读者加锁/解锁

2、写者加锁/解锁

三、读写锁的相关函数

1、初始化读写锁:pthread_rwlock_init

2、申请读者锁:pthread_rwlock_rdlock 

3、申请写者锁:pthread_rwlock_wrlock

4、解锁:pthread_rwlock_unlock

5、销毁读写锁:pthread_rwlock_destroy

四、读写锁的简单使用

五、注意事项


一、模型中的三种关系

两个角色指的是读者角色和写者角色,三种关系指的是读者和读者、写者和写者、读者和写者。

1、写者和写者

每次画黑板报的都只能有一个,两个人同时画的话,一个人画消防主题,另一个人却在画动物主题,这并不是我们想看到的。

==》写者和写者之间是互斥关系。必须要等一个人画完,才能让下一个画

2、读者和写者

有的时候,你可能认为,画黑板报和读黑板报是可以同时进行的,但是画的过程中,读的人可能无法get到你在画什么,明明你在画龙,读的人却说你在画蛇。这就引起误解了,所以应该等写者写完,读者再读。

另一个角度就是,读者还在读,写者却想把板报给擦了,很显然这就矛盾了,所以我们应该让读者读完,写者再写。

==》读者和写者之间是互斥关系。写者在写的时候,读者不能得到锁;读者在读的时候,写者也不能得到锁。

3、读者和读者

一个人读不会影响另一个人读,这里就不存在什么互斥关系了。

==》读者和读者不会互相影响

 

二、读写锁加锁/解锁的基本原理

读写锁使用读者的数目作为临界资源,即 int readers = 0;

1、读者加锁/解锁

因为读者和读者之间是没有关系的,所以加锁和解锁是两个独立的过程

加锁 = 读黑板报的人加1

解锁 = 读黑板报的人减1

// 加锁
lock();
readers++;
unlock();

// 解锁
lock();
readers--;
unlock();

2、写者加锁/解锁

写者申请锁的时候,就要看读者数量了,主要有两种情况

  • readers != 0:当有读者在的时候,写者就无法申请到锁,此时会进入条件变量等待
  • readers == 0:只有当读者为0时,此时写者会被唤醒去争抢锁
// 等待读者线程解锁
while(readers > 0)
{
    cond_wait();        // 进入条件变量等待(等到读者数目为0,发送信号将挂起的写者唤醒)
}

lock();          //加锁

modify();        //用来表示写者修改内容的操作

unlock();        //解锁

三、读写锁的相关函数

 从上面可以了解到,读锁和写锁本质上维护的是同一份临界资源。读者加锁的时候,读者数目加1;写者加锁的时候,必须要等到没有其他读者以后,才会去争抢锁。pthread库提供了 pthread_rwlock_t 这种结构体类型来表示读写锁。其他读写锁相关函数大致如下:

  • pthread_rwlock_init:初始化一个读写锁
  • pthread_rwlock_rdlock:读者申请锁
  • pthread_rwlock_tryrdlock:非阻塞读锁定  
  • pthread_rwlock_wrlock:写者申请锁
  • pthread_rwlock_trywrlock:非阻塞写锁定
  • pthread_rwlock_unlock:解锁读写锁
  • pthread_rwlock_destroy:释放读写锁

1、初始化读写锁:pthread_rwlock_init

pthread_rwlock_init 的作用是初始化一个读写锁。

第一个参数rwlock:你要初始化的读写锁。

第二个参数 attr:给读写锁设置属性。设为NULL的话表示使用默认属性。

返回值:成功返回0;失败返回一个错误码

2、申请读者锁:pthread_rwlock_rdlock 

pthread_rwlock_rdlock 的作用是申请读者锁,此时读者的数量会加1。

3、申请写者锁:pthread_rwlock_wrlock

pthread_rwlock_wrlock 的作用是申请写者锁,允许申请的条件是,读者的数量为0

4、解锁:pthread_rwlock_unlock

pthread_rwlock_unlock 的作用是为读者 / 写者锁解锁。因为读写锁保护的是同一个临界资源,解锁其实就是让这份临界资源可以被读者 / 写者访问到。

 

5、销毁读写锁:pthread_rwlock_destroy

pthread_rwlock_destroy 的作用是销毁读写锁。

参数rwlock:你要销毁的读写锁

返回值:成功返回0;失败返回一个错误码

 

四、读写锁的简单使用

现在有一个读线程,一个写线程,读线程在读数据的时候,写线程无法申请锁;写线程只能趁着读线程休眠的空挡来申请锁。

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

pthread_rwlock_t rwlock;

void* read_thread(void* args){
	 pthread_detach(pthread_self());

 	 while (1)
 	 {
		 pthread_rwlock_rdlock(&rwlock);
		 printf("读者[1]正在读内容\n");
		 pthread_rwlock_unlock(&rwlock);   
         sleep(1);
 	 }    
}

void* write_thread(void* args){
	pthread_detach(pthread_self());

	while (1)
	{
		pthread_rwlock_wrlock(&rwlock);
		printf("写者[1]正在写内容\n");
		pthread_rwlock_unlock(&rwlock);        
        // sleep(1);
	}
}

int main(){
	pthread_t tid1,tid2,tid3,tid4;
	pthread_rwlock_init(&rwlock, NULL);
	pthread_create(&tid1,NULL,read_thread, NULL);
	pthread_create(&tid3,NULL,write_thread, NULL);
	while(1){    
		sleep(1);
	} 
	return 0;
}

 

五、注意事项

同一时刻,只有一个线程可以获得写锁,但是可以有多个线程获得读锁。

  • 读写锁处于写锁状态时,所有要申请读写锁的线程都会被阻塞。
  • 读写锁处于读锁状态时,写锁申请会阻塞等待,读锁不会受到影响
<think> 我们正在讨论的是读写锁使用场景。根据引用内容,我们已经了解读写锁的基本原理和它在C++、Golang中的实现方式。现在需要总结并扩展读写锁的应用场景。 引用[1]提到:读写锁的核心思想是区分读操作的访问模式,在操作系统、数据库设计和分布式系统中都有广泛应用。例如Linux内核的rwlock_t和数据库的MVCC。 引用[2]说明了读写锁的三种状态以及状态转换,这为我们理解其适用场景提供了基础。 引用[3]则具体提到了连接池管理中使用读写锁来控制对连接资源的访问。 因此,我们可以将读写锁使用场景归纳为以下几类: 1. **读多少的并发数据结构**:当数据结构被频繁读取但较少修改时,使用读写锁可以显著提升并发性能。例如: - 缓存系统(如Redis的读模型) - 配置信息的热更新(多个线程读取配置,偶尔有线程更新配置) 2. **资源池管理**(如引用[3]中的连接池): - 数据库连接池:获取连接(保证独占分配),释放连接(可以使用,取决于实现) - 线程池、对象池等资源复用场景 3. **文件系统与操作系统内核**: - 如Linux内核中使用读写锁(rwlock_t)保护文件系统元数据、内存管理数据结构等 - 虚拟文件系统(VFS)中inode的访问控制 4. **数据库系统**: - 多版本并发控制(MVCC)中,读写锁可用于管理数据版本(虽然MVCC通常使用更精细的,但基本原理类似) - 数据库索引的维护(如B+树索引的读操作并发,操作独占) 5. **网络服务与高并发服务器**: - Web服务器中会话(session)数据的访问 - 实时数据推送服务中客户端连接列表的维护 6. **编译器和解释器**: - 符号表(symbol table)的并发访问(例如在JIT编译过程中) 7. **游戏开发**: - 游戏世界状态的分区管理(如多个线程读取游戏实体状态,主线程更新状态) 注意:读写锁并非万能,在以下场景可能不适用: - 操作非常频繁的场景(此时读写锁可能退化为互斥) - 需要保证操作优先的场景(可能发生线程饥饿) - 对实时性要求极高的场景(的开销可能无法接受) 因此,选择使用读写锁时,需要根据具体场景的读比例、性能要求、实时性要求进行权衡。 下面用Mermaid状态图展示读写锁的状态转换(基于引用[2]的描述,但做了一些修正和补充): </think> ### 读写锁的应用场景分析 读写锁(Read-Write Lock)的核心价值在于**区分读/操作的并发策略**,适用于读操作远多于操作的场景。以下是典型应用场景及技术原理: --- #### 一、基础应用场景 1. **高并发读取的共享数据** - **场景**:配置中心、全局缓存、元数据存储 - **原理**: - 多个线程同时读取数据(`std::shared_lock`) - 单线程独占入(`std::unique_lock`) - 如引用[1]描述的C++读写锁模型: ```c++ std::shared_mutex rw_lock; // 读操作 { std::shared_lock lock(rw_lock); // 共享 read_data(); } // 操作 { std::unique_lock lock(rw_lock); // 独占 modify_data(); } ``` - **优势**:读操作并发性提升10-100倍(Bench实测)[^1] 2. **资源池管理** - **场景**:数据库连接池(如引用[3])、线程池 - **原理**: - 获取资源时:保证原子分配 - 释放资源时:读允许并行回收 - 状态转换如引用[2]的Mermaid描述: ```mermaid stateDiagram-v2 [*] --> 无状态 无状态 --> 读状态 : 获取读状态 --> 状态 : 获取状态 --> 读状态 : 再次获取读状态 --> 状态 : 所有读释放后获取 状态 --> 无状态 : 释放 ``` --- #### 二、高级系统场景 3. **文件系统与内核** - **场景**:Linux内核`rwlock_t`、文件元数据更新 - **原理**: - 多进程并行读取inode(读) - 单进程修改文件大小/权限() - **优势**:避免元数据竞争导致文件损坏 4. **数据库并发控制** - **场景**:多版本并发控制(MVCC) - **原理**: - 读操作:快照读(类似读,不阻塞) - 操作:独占版本链修改(类似) - **性能**:OLTP系统吞吐量提升30%+(MySQL实测) 5. **实时数据处理** - **场景**:金融行情分发、物联网传感器聚合 - **模式**: - 多消费者并行读取最新数据(读) - 单生产者更新数据源() - **时延**:读操作延迟<1μs(DPDK优化场景) --- #### 三、替代方案考量 | **场景特征** | **推荐方案** | **原因** | |---------------------|--------------------|------------------------------| | 读比例>100:1 | 读写锁 | 最大化读并发[^1] | | 操作频繁 | 互斥 | 避免饥饿问题 | | 超高吞吐要求 | 无数据结构 | 消除开销(如RCU、CAS) | | 分布式系统 | 租约机制 | 跨节点协调(如Chubby) | > 读写锁在**读密集型场景**的收益最显著,但当操作超过20%时,互斥可能更优(Google Abseil库基准测试)。 --- ### 总结:核心适用场景 1. **读多少**:配置管理、缓存系统 2. **资源池化**:连接池、线程池 3. **元数据操作**:文件系统、数据库索引 4. **实时数据分发**:金融/物联网流处理 当需要平衡**并发性能**与**数据一致性**时,读写锁是经典选择,但需警惕操作优先级反转等问题[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值