第4章 读写锁与共享锁:读读不互斥、读写互斥、写写互斥

在并发访问共享资源时,锁的互斥粒度直接决定了系统的吞吐能力与数据安全性。读写锁(Readers-Writer Lock)是对传统互斥锁的重要优化,它基于访问类型而非线程身份进行控制,提供三种核心语义:读读不互斥、读写互斥、写写互斥。本章从架构层面拆解这些语义如何影响模块边界、组件职责与调度路径。

互斥关系 并发能力 数据安全压力 典型场景
读读不互斥 完全并发 低(只读) 缓存读取、配置查询
读写互斥 完全串行 中(写可能看到旧读) 读写混合的高频查询
写写互斥 完全串行 高(写写冲突) 计数器更新、状态变更
架构启示: 读写锁的本质是将“访问类型”作为关注点分离的杠杆,在读多写少的边界处,它比互斥锁提升 2~10 倍的并发能力,但代价是写操作的公平性和饥饿风险。
读写锁调度状态图 锁管理器 读线程A 读线程B 写线程X 写线程Y 读等待队列 写等待队列 共享资源 读请求组 写请求组 说明:读读不互斥(多个读可并行);读写互斥(读时写被阻塞);写写互斥(同一时间仅一个写授权)

模块边界

读写锁的边界应划定在读多写少的组件交界处。模块 A 提供只读查询接口,模块 B 提供状态更新接口,两者通过同一读写锁共享数据容器。边界处只暴露锁的读/写获取方法,不暴露内部互斥策略。

  • 读边界:允许任意数量并发读,无数据竞争。
  • 写边界:确保写操作排它,写完成后读马上可见。
  • 边界契约:读期间禁止写,写期间禁止读写。

组件职责

  1. 读计数器:记录当前活跃读线程数量;在锁管理器中内聚。
  2. 写标志位:指示是否有写线程持有锁;写线程退出时清零。
  3. 等待队列:分别维护读等待与写等待队列,避免线程轮询。
  4. 锁管理器:根据当前状态决定新请求是直接放行、排队还是唤醒。

数据流与控制流

读请求数据流: 读线程调用 acquire_read → 锁管理器检查写标志位为空 → 读计数器递增 → 直接访问共享资源 → release_read → 读计数器递减 → 若为 0 且写队列非空,则唤醒一个写线程。

写请求控制流: 写线程调用 acquire_write → 锁管理器检查读计数器=0 且写标志位=0 → 写标志位置位 → 排它访问共享资源 → release_write → 写标志位清零 → 按策略唤醒读或写等待队列。

关键路径: 读读不互斥使得读请求完全并行,控制流只需 O(1) 检查标志位;读写互斥控制流需要挂起/唤醒操作,上下文切换代价较高。

选型权衡

维度 读写锁 细粒度锁
写公平性 可能写饥饿(读源源不断) 均等竞争
读吞吐量 极高(并行读) 中等(分区竞争)
实现复杂度 低(标准库内置) 高(需要分区算法)
适用场景 读远大于写 读写比例均衡
风险点:
  • 写优先级反转: 当读线程持续进入时,写线程可能无限期等待;需使用公平策略或写偏爱锁。
  • 读锁升级: 在持有读锁时试图获取写锁,极易造成死锁;架构层禁止此操作。
  • 锁降级: 写锁释放前应降级为读锁以降低调度开销,但需确保事务原子性。

落地步骤

  1. 识别读多写少的模块边界(如配置中心、元数据缓存)。
  2. 选择语言标准库的读写锁实现(如 std::shared_mutex、pthread_rwlock)。
  3. 封装锁接口为显式读/写操作,避免调用方直接操作锁对象。
  4. 设置写偏爱或读偏爱策略,根据业务对公平性的要求调整。
  5. 压测对比互斥锁,验证读并发提升是否达到期望。

验收标准

  • 并发 10 个读线程时,延迟低于 2 ms,且无写线程明显阻塞。
  • 写操作期间,所有读线程必须全部阻塞(可接受的最大延迟小于 5 ms)。
  • 连续 1000 次写后,数据一致性与无锁场景一致。
  • 锁管理器无死锁、无饥饿报告(运行 24 h 无异常)。
验证手段: 使用线程安全分析工具(如 ThreadSanitizer)检测数据竞争;通过火焰图观察读路径是否并行化。