1. 读写锁基础
在多线程/多核并发编程中,保护共享数据是核心挑战。当数据呈现“多读少写”的特征时,传统的互斥锁(mutex)会因过度串行化而成为性能瓶颈。读写锁(rwlock)正是为解决此问题而设计的同步原语。
1.1 读写锁(rwlock)的定义
读写锁是一种允许多个读者同时访问共享资源,但只允许一个写者独占访问的同步机制。它维护了两种锁状态:读模式锁和写模式锁。
- 读模式锁(共享锁):当没有写者持有锁时,任意数量的读者可以同时获取读锁。读者之间不互斥。
- 写模式锁(独占锁):当没有读者和写者持有锁时,写者可以获取写锁。写者与其他所有读者和写者都互斥。
核心原则:读读不互斥,读写互斥,写写互斥。
1.2 读写锁与互斥锁的核心区别
互斥锁(mutex)是最简单的锁,它只有“锁定”和“未锁定”两种状态。无论线程是读还是写,都必须独占锁。这意味着即使多个线程只读取数据,也必须串行执行,无法利用硬件的并行读取能力。
下表从多个维度对比了读写锁与互斥锁的差异:
| 对比维度 | 互斥锁(mutex) | 读写锁(rwlock) |
|---|---|---|
| 并发读 | 不允许,读操作串行化 | 允许,多个读者可同时进入 |
| 并发写 | 不允许,写操作串行化 | 不允许,写操作串行化 |
| 读写并发 | 不允许 | 不允许(写者优先或读者优先策略) |
| 锁开销 | 较低(状态简单) | 较高(需维护读者计数和写者状态) |
| 适用场景 | 读写比例接近,或写操作频繁 | 读操作远多于写操作(多读少写) |
| 公平性 | 通常公平(FIFO 或按优先级) | 可能产生写者饥饿(读者优先策略下) |
性能权衡:读写锁通过增加锁管理的复杂度(维护读者计数)来换取更高的读并发度。在“多读少写”场景下,这种权衡是值得的;但在写操作频繁时,读写锁的开销可能超过互斥锁。
1.3 适用场景分析:多读少写
读写锁最适合以下特征的工作负载:
- 读操作占比极高:通常读操作占总访问量的 80% 以上,甚至 99%。
- 写操作不频繁且短暂:写操作发生频率低,且执行时间短。
- 数据一致性要求严格:写操作必须立即对后续读操作可见。
典型的应用场景包括:
- 系统配置表:系统启动时加载配置,运行时大部分线程只读取配置,极少修改。
- 路由表/转发表:网络设备中的路由表,数据包转发时频繁查询,路由协议更新时偶尔修改。
- 文件系统元数据缓存:如 inode 缓存、dentry 缓存,读操作远多于写操作。
- 统计计数器:多个线程累加统计值,但偶尔需要重置或读取快照。
避坑指南:如果写操作比例超过 20%,或者写操作持有锁的时间较长,读写锁的性能可能反而低于互斥锁。因为写锁的独占性会导致读者被阻塞,而读写锁的额外开销(如原子操作维护读者计数)会放大性能损失。
1.4 读写锁的工作流程
下图展示了读写锁在典型“多读少写”场景下的工作流程:
1.5 Linux 内核中的读写锁接口
Linux 内核提供了两种主要的读写锁实现:经典读写锁(rwlock_t)和RCU(Read-Copy-Update)。本章聚焦于经典读写锁,其核心 API 如下:
// 定义并初始化读写锁
rwlock_t my_rwlock = __RW_LOCK_UNLOCKED(my_rwlock);
// 读模式操作
read_lock(&my_rwlock); // 获取读锁(自旋等待)
read_unlock(&my_rwlock); // 释放读锁
// 写模式操作
write_lock(&my_rwlock); // 获取写锁(自旋等待)
write_unlock(&my_rwlock); // 释放写锁
// 中断安全版本(用于中断上下文)
unsigned long flags;
read_lock_irqsave(&my_rwlock, flags);
// ... 临界区 ...
read_unlock_irqrestore(&my_rwlock, flags);
write_lock_irqsave(&my_rwlock, flags);
// ... 临界区 ...
write_unlock_irqrestore(&my_rwlock, flags);
内核开发者提示:在中断上下文中使用读写锁时,务必使用
_irqsave / _irqrestore 变体,以避免死锁。这些变体会在获取锁前保存本地中断状态,并在释放锁后恢复。
1.6 读写锁的公平性策略
读写锁的实现通常有两种公平性策略:
- 读者优先:只要存在读者,写者就无法获取锁。这种策略可能导致写者饥饿(starvation),即写者长时间无法获得锁。
- 写者优先:当写者等待时,新的读者不能获取读锁,必须等待写者完成。这种策略避免了写者饥饿,但可能降低读并发度。
Linux 内核的经典读写锁(rwlock_t)默认采用读者优先策略。在后续章节中,我们将深入探讨如何通过调整策略来优化特定场景的性能。
本章小结:读写锁通过区分读操作和写操作,允许多个读者并行执行,从而在“多读少写”场景下显著提升性能。与互斥锁相比,读写锁增加了锁管理的复杂度,但换来了更高的读并发度。理解读写锁的定义、区别和适用场景,是后续进行性能调优的基础。