4、读写锁原理:读共享写互斥,公平性怎么破?

读写锁这东西,说白了就是「看人下菜碟」。

我刚开始接触并发编程时,总觉得锁就是锁,加就完了。后来在一个高并发缓存系统里,发现读操作占了90%以上,写操作寥寥无几。如果全用互斥锁,那读线程之间互相阻塞,性能简直惨不忍睹。嗯,这时候读写锁就派上用场了。

4.1 设计思想:读共享,写互斥

读写锁的核心思想其实很简单:

  • 读锁(共享锁):多个线程可以同时持有读锁。你读你的,我读我的,互不干扰。
  • 写锁(独占锁):写锁只能被一个线程持有。写的时候,谁都不能读,谁都不能写。

你想想看,这就像图书馆的阅览室。大家都可以进去看书(读共享),但如果有管理员要整理书架(写操作),那就得清场,所有人都得出去。

核心规则:

  • 读锁和读锁可以共存
  • 读锁和写锁互斥
  • 写锁和写锁互斥

我在项目中遇到过一个问题:一个配置中心服务,配置几乎不变,但每秒有上万次读取。如果用互斥锁,CPU直接飙到80%。换成读写锁后,读性能几乎无损耗,CPU降到15%。这就是读写锁的价值所在。

4.2 读共享写互斥的底层实现

读写锁内部通常维护两个关键状态:

  • 读锁计数:当前有多少线程持有读锁
  • 写锁标志:当前是否有线程持有写锁

加锁逻辑大致是这样的:

// 伪代码,展示核心逻辑
class ReadWriteLock:
    read_count = 0
    write_flag = False
    mutex = Mutex()  // 保护内部状态的互斥锁

    def acquire_read(self):
        while write_flag:
            // 有写锁,等待
            wait()
        read_count += 1

    def release_read(self):
        read_count -= 1
        if read_count == 0:
            notify_waiters()

    def acquire_write(self):
        while write_flag or read_count > 0:
            // 有读锁或写锁,等待
            wait()
        write_flag = True

    def release_write(self):
        write_flag = False
        notify_all_waiters()

注意,这只是最朴素的实现。实际生产环境要考虑更多细节,比如防止写锁饥饿。

4.3 读写锁的公平性问题

这里有个坑,我踩过。

假设现在有100个读线程正在读,突然来了一个写线程。按照规则,写线程必须等所有读线程释放锁。但问题是,如果读线程源源不断地进来,写线程可能永远拿不到锁——这就是写锁饥饿。

反过来,如果写线程频繁,读线程也可能被饿死。

解决公平性问题,常见策略有三种:

策略 描述 优缺点
强读优先 读锁永远优先,写锁必须等所有读锁释放 读性能好,写可能饿死
强写优先 一旦有写锁等待,新读锁必须等待 写性能好,读可能饿死
公平队列 按请求顺序排队,先来后到 公平,但吞吐量可能下降

我个人习惯用公平队列策略。虽然吞吐量会损失一点,但至少不会出现某个线程永远拿不到锁的极端情况。曾经在一个金融交易系统里,就因为写锁饥饿导致配置更新延迟了十几秒,差点出事故。

避坑指南:我曾经在一个高并发场景下用了强读优先的读写锁,结果写操作平均等待时间超过2秒。后来改成公平队列,写等待降到50ms以内,读性能只下降了不到5%。这个取舍是值得的。

4.4 Python中读写锁的实现

Python标准库没有直接提供读写锁,但我们可以自己封装。Python的threading模块提供了Lock和Condition,足够我们实现一个读写锁。

下面是我在项目中用过的实现:

import threading

class ReadWriteLock:
    def __init__(self):
        self._read_ready = threading.Condition(threading.Lock())
        self._readers = 0

    def acquire_read(self):
        with self._read_ready:
            self._readers += 1

    def release_read(self):
        with self._read_ready:
            self._readers -= 1
            if self._readers == 0:
                self._read_ready.notify_all()

    def acquire_write(self):
        self._read_ready.acquire()
        while self._readers > 0:
            self._read_ready.wait()

    def release_write(self):
        self._read_ready.release()

这个实现是「写优先」的。为什么?因为acquire_write先获取了底层的Lock,后续的acquire_read会被阻塞在with self._read_ready处。这样写线程就能优先拿到锁。

如果你想要公平队列版本,可以引入一个threading.Semaphore或者用queue.Queue来排队。不过说实话,对于大多数Python应用,上面的简单实现已经够用了。

小提示:Python的GIL(全局解释器锁)决定了同一时刻只有一个线程在执行Python字节码。所以读写锁在Python中主要解决的是「逻辑并发」问题,而不是真正的CPU并行。但对于I/O密集型任务,效果依然显著。

4.5 知识体系总览

下面这张图,是我梳理的读写锁核心知识脉络:

读写锁知识体系 读写锁 设计思想 读共享 写互斥 公平性问题 读优先 写优先 公平队列 Python实现 Condition实现 写优先策略 核心:读不阻塞读,写阻塞一切

这张图把读写锁的三个核心维度串起来了。设计思想是基础,公平性是工程权衡,Python实现是落地手段。三者缺一不可。

最后说一句:读写锁不是银弹。如果你的场景读写比例接近1:1,或者写操作非常频繁,那读写锁可能还不如互斥锁。我建议你在引入读写锁之前,先用性能分析工具跑一下,看看读写比例到底是多少。数据说话,别靠感觉。

可可精品资料库