3. 读写锁原理:读者-写者模型、读锁共享、写锁互斥、公平性策略

读写锁(Read-Write Lock)是并发编程中一种专门优化“读多写少”场景的同步原语。与互斥锁(Mutex)的“完全互斥”不同,读写锁区分了两种锁获取模式:读模式(共享)和写模式(独占)。这种设计允许更高的并发度,因为多个读者可以同时访问共享数据,而写者则必须独占访问权。

3.1 读者-写者模型

读者-写者模型是读写锁的理论基础。该模型将并发访问共享资源的线程分为两类:

  • 读者(Reader):只读取共享数据,不修改。多个读者可以同时访问,不会产生数据竞争。
  • 写者(Writer):修改共享数据。写者必须独占访问权,任何其他读者或写者都不能同时访问。

读写锁的核心约束如下表所示:

当前持有者 请求者类型 是否允许 说明
读者(任意数量) 读者 允许 读锁共享,多个读者可同时持有
读者(任意数量) 写者 阻塞 写者必须等待所有读者释放锁
写者(唯一) 读者 阻塞 读者必须等待写者释放锁
写者(唯一) 写者 阻塞 写锁互斥,只允许一个写者
性能洞察: 在读者占绝大多数的场景中(如配置读取、路由表查询、缓存命中),读写锁可以将吞吐量提升数倍甚至数十倍,因为读操作不再串行化。

3.2 读锁共享与写锁互斥

读写锁的核心机制体现在两个关键操作中:读锁获取和写锁获取。

3.2.1 读锁共享(Shared Mode)

当线程请求读锁时,只要当前没有写者持有锁(且没有写者在等待队列中,取决于公平性策略),该线程就可以立即获得读锁。多个读者可以同时持有读锁,每个读者都会增加一个内部计数器(read_count)。

典型的读锁获取伪代码如下:

// 读锁获取(简化实现)
void read_lock(rwlock_t *lock) {
    spin_lock(&lock->wait_lock);
    // 等待直到没有写者持有锁
    while (lock->writer_held) {
        // 将当前线程加入读者等待队列
        wait_queue_add(&lock->read_wait, current);
        spin_unlock(&lock->wait_lock);
        schedule(); // 睡眠等待
        spin_lock(&lock->wait_lock);
    }
    lock->read_count++; // 读者计数+1
    spin_unlock(&lock->wait_lock);
}

// 读锁释放
void read_unlock(rwlock_t *lock) {
    spin_lock(&lock->wait_lock);
    lock->read_count--;
    if (lock->read_count == 0 && lock->write_waiting) {
        // 唤醒等待的写者
        wake_up(&lock->write_wait);
    }
    spin_unlock(&lock->wait_lock);
}

3.2.2 写锁互斥(Exclusive Mode)

当线程请求写锁时,它必须等待所有读者和所有其他写者释放锁。写锁是严格互斥的,同一时刻只能有一个写者持有。

写锁获取伪代码:

// 写锁获取(简化实现)
void write_lock(rwlock_t *lock) {
    spin_lock(&lock->wait_lock);
    // 标记有写者在等待
    lock->write_waiting = 1;
    // 等待直到没有读者且没有其他写者
    while (lock->read_count > 0 || lock->writer_held) {
        wait_queue_add(&lock->write_wait, current);
        spin_unlock(&lock->wait_lock);
        schedule();
        spin_lock(&lock->wait_lock);
    }
    lock->writer_held = 1;  // 标记写者持有
    lock->write_waiting = 0;
    spin_unlock(&lock->wait_lock);
}

// 写锁释放
void write_unlock(rwlock_t *lock) {
    spin_lock(&lock->wait_lock);
    lock->writer_held = 0;
    // 优先唤醒读者(读者优先策略)或写者(写者优先策略)
    wake_up_all(&lock->read_wait);
    wake_up(&lock->write_wait);
    spin_unlock(&lock->wait_lock);
}
读写锁状态转换图 无锁(UNLOCKED) read_count=0, writer=0 读锁(READ LOCKED) read_count=N, writer=0 写锁(WRITE LOCKED) read_count=0, writer=1 读锁获取 写锁获取 新读者加入 所有读者释放 写者释放 写者等待(阻塞) 读者等待(阻塞) 状态说明: • 无锁 → 读锁:第一个读者获取读锁,read_count 从 0 变为 1。 • 读锁 → 读锁:后续读者直接加入,read_count 递增,无需等待。 • 读锁 → 写锁:写者必须等待 read_count 降为 0,期间新读者可能继续加入(取决于策略)。 • 写锁 → 读锁:读者必须等待 writer_held 变为 0,写锁释放后唤醒所有等待读者。

3.3 公平性策略

读写锁的公平性策略决定了当写者等待时,新到达的读者是否能够立即获取读锁。不同的策略对性能和公平性有显著影响。

3.3.1 读者优先(Reader-Preference)

读者优先策略允许新读者在写者等待时仍然获取读锁。这种策略最大化读并发性能,但可能导致写者饥饿(Starvation)——如果读者持续到达,写者可能永远无法获得锁。

  • 优点:读操作延迟最低,吞吐量最高。
  • 缺点:写者可能无限期等待,不适合写操作有实时性要求的场景。
  • 适用场景:配置读取、统计信息收集等写操作极少且不紧急的场景。

3.3.2 写者优先(Writer-Preference)

写者优先策略在写者等待时,阻塞新读者的读锁请求。一旦有写者开始等待,后续读者必须排队,直到所有等待的写者完成。这保证了写者不会饥饿,但会降低读并发度。

  • 优点:写者延迟可控,避免饥饿。
  • 缺点:读操作可能被阻塞,读吞吐量下降。
  • 适用场景:数据库事务日志、文件系统元数据更新等写操作需要及时响应的场景。

3.3.3 公平锁(Fair Locking / 排队策略)

公平锁使用一个等待队列,所有请求(读或写)按照到达顺序排队。当锁释放时,队首的请求获得锁。如果队首是读者,则允许所有连续的读者一起进入;如果队首是写者,则只允许该写者进入。

  • 优点:读写公平,无饥饿,延迟可预测。
  • 缺点:读并发度受限于写者到达频率,整体吞吐量可能低于读者优先。
  • 适用场景:实时系统、需要严格公平性的通用场景。
内核实践: Linux 内核的 rwlock_t 默认采用读者优先策略,因为内核中读操作远多于写操作,且写者通常能快速完成。而 POSIX 线程库的 pthread_rwlockattr_setkind_np() 允许用户选择 PTHREAD_RWLOCK_PREFER_READER_NP、PTHREAD_RWLOCK_PREFER_WRITER_NP 或 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 策略。

3.4 三种公平性策略对比

特性 读者优先 写者优先 公平锁(排队)
写者饥饿风险 高 无 无
读并发度 最高 中等 中等
写者延迟 不可预测 低且可控 中等且可控
实现复杂度 低 中 高
典型应用 配置缓存、路由表 数据库日志、文件系统 通用中间件、实时系统
三种公平性策略下的锁获取时序对比 读者优先: 读者A(持有) 读者B(持有) 写者W(等待) 读者C(进入) 写者W(持有) 时间→ 写者优先: 读者A(持有) 读者B(持有) 写者W(等待) 读者C(阻塞) 写者W(持有) 时间→ 公平锁(排队): 读者A(持有) 读者B(持有) 写者W(排队) 读者C(排队) 写者W(持有) 时间→ 关键差异: • 读者优先:读者C在写者W等待时仍能进入,导致写者W等待时间延长。 • 写者优先:读者C被阻塞,写者W一旦获得锁即可立即执行,延迟可控。 • 公平锁:所有请求按到达顺序排队,写者W先于读者C获得锁。

3.5 读写锁的性能权衡

选择读写锁策略时,需要根据实际工作负载进行权衡:

  • 读/写比例:读操作占比越高,读者优先策略的优势越明显。当读占比超过 90% 时,读写锁相比互斥锁通常能带来 5-10 倍的性能提升。
  • 临界区大小:临界区执行时间越长,锁竞争越激烈,公平性策略的影响越大。
  • 写操作频率:写操作越频繁,写者优先或公平锁越重要,以避免写者饥饿。
  • 实时性要求:如果写操作有 deadline 要求,必须使用写者优先或公平锁。
常见陷阱: 不要在所有场景下都使用读写锁替代互斥锁。当临界区执行时间极短(如仅操作一个原子变量)时,读写锁的额外开销(维护读者计数、唤醒逻辑)可能超过互斥锁,导致性能反而下降。始终通过基准测试验证性能收益。

3.6 本章小结

读写锁通过区分读者和写者,实现了读锁共享和写锁互斥,是优化“读多写少”场景的核心工具。公平性策略(读者优先、写者优先、公平锁)决定了读写锁在不同负载下的行为特征。理解这些原理是后续学习 RCU(Read-Copy-Update)机制的基础——RCU 在读写锁的基础上进一步消除了读者端的锁开销,实现了真正的无等待读操作。