2. 读写锁原理

读写锁(Read-Write Lock)是并发控制中一种精细化的同步原语,它区分了“读”与“写”两种操作,允许多个读者同时访问共享资源,但写者必须独占访问。这种机制在“读多写少”的场景下,能显著提升并发性能,是内核与高性能中间件中不可或缺的基石。

读写锁的语义与状态机

读写锁的核心语义可以用一个简单的状态机来概括。锁的持有状态分为三种:空闲、读锁定、写锁定。

核心语义规则:
  • 读-读共享:任意数量的读者可以同时持有读锁。
  • 读-写互斥:只要有一个读者持有锁,写者就必须等待;反之亦然。
  • 写-写互斥:写者之间完全互斥,同一时刻只能有一个写者。
读写锁状态转换图 空闲 读锁定 写锁定 读请求 写请求 新读者加入 所有读者释放 写者释放 不可直接升级 说明:读锁不能直接升级为写锁,必须先释放读锁,再申请写锁。 这种设计避免了复杂的死锁与锁降级问题。

公平性与写饥饿问题

读写锁的语义虽然清晰,但在实际调度中,公平性是一个必须正视的问题。如果设计不当,写者可能被源源不断的读者“饿死”。

调度策略 公平性描述 写饥饿风险 典型应用场景
读者优先 只要存在读者,写者就必须等待所有当前及后续读者释放锁。 高 对读延迟极度敏感,写操作极少且可容忍延迟。
写者优先 一旦有写者等待,新读者将被阻塞,直到所有等待写者完成。 低 写操作需要及时响应,读操作可容忍短暂阻塞。
公平排队 按照请求到达的先后顺序(FIFO)授予锁,读写请求平等排队。 极低 对读写延迟都有明确SLA要求的通用系统。
工程陷阱:在读者优先策略下,如果读操作持续不断(例如高频监控线程),写者可能永远无法获得锁,导致数据更新停滞。这是典型的“写饥饿”问题。解决思路是引入“写者偏向”或“排队公平”机制。

读写锁在内核中的实现

Linux 内核提供了多种读写锁实现,以适应不同上下文(进程上下文 vs 中断上下文)和性能要求。

1. 经典读写自旋锁 (rwlock_t)

适用于短临界区,不允许睡眠。读者和写者通过原子操作竞争一个 32 位计数器。

// 内核源码: include/linux/rwlock_types.h
typedef struct {
    arch_rwlock_t raw_lock;
} rwlock_t;

// 使用示例
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);

read_lock(&my_rwlock);   // 读者获取锁
// ... 读临界区 ...
read_unlock(&my_rwlock);

write_lock(&my_rwlock);  // 写者获取锁
// ... 写临界区 ...
write_unlock(&my_rwlock);

2. 读写信号量 (struct rw_semaphore)

适用于长临界区,允许写者睡眠等待。内核中广泛用于文件系统和内存管理。

// 内核源码: include/linux/rwsem.h
struct rw_semaphore {
    atomic_long_t count;
    struct list_head wait_list;
    // ...
};

// 使用示例
struct rw_semaphore my_rwsem;
init_rwsem(&my_rwsem);

down_read(&my_rwsem);   // 读者获取锁(可睡眠)
// ... 读临界区 ...
up_read(&my_rwsem);

down_write(&my_rwsem);  // 写者获取锁(可睡眠)
// ... 写临界区 ...
up_write(&my_rwsem);

3. RCU (Read-Copy-Update)

一种无锁的读端同步机制,读者几乎无开销,写者通过“发布-订阅”和“宽限期回收”实现更新。这是本课程后续章节的核心内容。

读写锁在用户态的实现

用户态程序通常通过 POSIX 线程库(pthreads)使用读写锁。其实现依赖于内核的 futex 机制,在无竞争时通过原子操作完成,有竞争时陷入内核睡眠。

// POSIX 读写锁示例
#include <pthread.h>

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

void reader_thread() {
    pthread_rwlock_rdlock(&rwlock);
    // 读取共享数据
    pthread_rwlock_unlock(&rwlock);
}

void writer_thread() {
    pthread_rwlock_wrlock(&rwlock);
    // 修改共享数据
    pthread_rwlock_unlock(&rwlock);
}

下表对比了内核态与用户态读写锁的关键差异:

特性 内核态 (rwlock_t / rwsem) 用户态 (pthread_rwlock_t)
底层机制 原子操作 + 自旋 / 等待队列 原子操作 + futex 系统调用
睡眠能力 rwlock_t 不可睡眠;rwsem 可睡眠 可睡眠(通过 futex)
公平性策略 通常为写者优先(避免饥饿) 实现依赖 glibc 版本,多为写者优先
适用场景 驱动、文件系统、核心调度 通用多线程应用、数据库、缓存
性能调优点 临界区长度、CPU 亲和性、NUMA 感知 锁粒度、读写比例、线程数

读写锁 vs RCU:一个初步的对比

在进入 RCU 的深入学习之前,先建立一个宏观对比,有助于理解为什么 RCU 在某些场景下能取代读写锁。

读写锁 vs RCU 架构对比 读写锁模型 读者 1 读者 2 写者 等待 读者与写者互斥,写者可能饥饿 RCU 模型 读者 1 读者 2 写者 宽限期 读者无锁访问,写者通过宽限期回收旧数据 演进
工程建议:在选择同步机制时,先评估读写比例。如果读操作占比超过 90% 且写操作需要低延迟,优先考虑 RCU。如果写操作频繁或临界区逻辑复杂,经典的读写锁(尤其是写者优先版本)仍然是可靠的选择。