1. 读写锁基础

在多线程/多核并发编程中,保护共享数据是核心挑战。当数据呈现“多读少写”的特征时,传统的互斥锁(mutex)会因过度串行化而成为性能瓶颈。读写锁(rwlock)正是为解决此问题而设计的同步原语。

1.1 读写锁(rwlock)的定义

读写锁是一种允许多个读者同时访问共享资源,但只允许一个写者独占访问的同步机制。它维护了两种锁状态:读模式锁和写模式锁。

  • 读模式锁(共享锁):当没有写者持有锁时,任意数量的读者可以同时获取读锁。读者之间不互斥。
  • 写模式锁(独占锁):当没有读者和写者持有锁时,写者可以获取写锁。写者与其他所有读者和写者都互斥。
核心原则:读读不互斥,读写互斥,写写互斥。

1.2 读写锁与互斥锁的核心区别

互斥锁(mutex)是最简单的锁,它只有“锁定”和“未锁定”两种状态。无论线程是读还是写,都必须独占锁。这意味着即使多个线程只读取数据,也必须串行执行,无法利用硬件的并行读取能力。

下表从多个维度对比了读写锁与互斥锁的差异:

对比维度 互斥锁(mutex) 读写锁(rwlock)
并发读 不允许,读操作串行化 允许,多个读者可同时进入
并发写 不允许,写操作串行化 不允许,写操作串行化
读写并发 不允许 不允许(写者优先或读者优先策略)
锁开销 较低(状态简单) 较高(需维护读者计数和写者状态)
适用场景 读写比例接近,或写操作频繁 读操作远多于写操作(多读少写)
公平性 通常公平(FIFO 或按优先级) 可能产生写者饥饿(读者优先策略下)
性能权衡:读写锁通过增加锁管理的复杂度(维护读者计数)来换取更高的读并发度。在“多读少写”场景下,这种权衡是值得的;但在写操作频繁时,读写锁的开销可能超过互斥锁。

1.3 适用场景分析:多读少写

读写锁最适合以下特征的工作负载:

  • 读操作占比极高:通常读操作占总访问量的 80% 以上,甚至 99%。
  • 写操作不频繁且短暂:写操作发生频率低,且执行时间短。
  • 数据一致性要求严格:写操作必须立即对后续读操作可见。

典型的应用场景包括:

  • 系统配置表:系统启动时加载配置,运行时大部分线程只读取配置,极少修改。
  • 路由表/转发表:网络设备中的路由表,数据包转发时频繁查询,路由协议更新时偶尔修改。
  • 文件系统元数据缓存:如 inode 缓存、dentry 缓存,读操作远多于写操作。
  • 统计计数器:多个线程累加统计值,但偶尔需要重置或读取快照。
避坑指南:如果写操作比例超过 20%,或者写操作持有锁的时间较长,读写锁的性能可能反而低于互斥锁。因为写锁的独占性会导致读者被阻塞,而读写锁的额外开销(如原子操作维护读者计数)会放大性能损失。

1.4 读写锁的工作流程

下图展示了读写锁在典型“多读少写”场景下的工作流程:

读写锁工作流程图 读写锁工作流程:多读少写场景 读者线程 A 尝试获取读锁 检查写者状态 无写者持有锁 → 获取读锁成功 读者 B 同时进入 读者计数 +1,不阻塞 释放读锁 读者计数 -1 写者线程 C 尝试获取写锁 检查读者计数 有读者持有锁 → 写者阻塞等待 等待所有读者释放 读者计数变为 0 获取写锁,独占访问 其他读者和写者均被阻塞 读者释放后唤醒 关键点:读者之间不互斥,可并行执行;写者必须等待所有读者释放后才能获得写锁

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)默认采用读者优先策略。在后续章节中,我们将深入探讨如何通过调整策略来优化特定场景的性能。

本章小结:读写锁通过区分读操作和写操作,允许多个读者并行执行,从而在“多读少写”场景下显著提升性能。与互斥锁相比,读写锁增加了锁管理的复杂度,但换来了更高的读并发度。理解读写锁的定义、区别和适用场景,是后续进行性能调优的基础。