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