线程同步之读写锁:读多写少的场景利器
说到线程同步,大家第一反应肯定是互斥锁。嗯,这没错。但我在实际项目中遇到过一种场景——读操作远远多于写操作。比如一个全局配置表,几十个线程都在读,只有偶尔一个线程在更新。这时候用互斥锁,所有读线程都得排队,性能就浪费了。
读写锁就是为这种场景设计的。它把锁分成了两种模式:读锁和写锁。说白了,就是「大家一起读没问题,但写的时候谁都不能碰」。
读写锁的原理
读写锁的核心思想其实很简单:
- 读锁(共享锁):多个线程可以同时持有读锁。只要没有线程持有写锁,读锁就能被任意多个线程获取。
- 写锁(独占锁):写锁是排他的。一个线程持有写锁时,其他线程既不能获取读锁,也不能获取写锁。
你想想看,这就像图书馆的阅览室。大家都可以进去看书(读锁),但只有一个人能进去整理书架(写锁),整理的时候其他人得在外面等着。
关键点:读写锁的「读」和「写」是互斥的,但「读」和「读」是共享的。这就是它比互斥锁高效的根本原因。
我个人的习惯是,在系统设计初期就判断数据访问模式。如果读操作占比超过80%,读写锁基本就是最优解。
读锁与写锁的区别
这里我整理了一个对比表,方便你直观理解:
| 特性 | 读锁 | 写锁 |
|---|---|---|
| 共享性 | 共享(多个线程可同时持有) | 独占(同一时间只能一个线程持有) |
| 获取条件 | 没有线程持有写锁 | 没有线程持有读锁或写锁 |
| 典型场景 | 读取配置、查询数据 | 更新配置、修改数据 |
| 性能影响 | 低(并发读) | 高(阻塞所有其他线程) |
| 死锁风险 | 较低 | 较高(尤其读锁升级写锁时) |
这里有个坑我要提醒你。我曾经在项目中犯过一个错误——在持有读锁的情况下尝试获取写锁。这会导致死锁。为什么?因为读锁是共享的,你拿着读锁等写锁,而写锁需要等所有读锁释放。你自己就把自己堵死了。
注意:不要尝试将读锁「升级」为写锁。POSIX 标准没有定义这种行为,不同平台表现可能不同。正确的做法是先释放读锁,再获取写锁。
读写锁的应用场景
读写锁不是万能的。它最适合「读多写少」的场景。我举几个实际例子:
- 配置管理系统:系统启动时加载配置,运行时多个线程频繁读取,管理员偶尔修改配置。
- DNS缓存:大量查询请求读取缓存,只有缓存过期时才写入新记录。
- 路由表:数据包转发时频繁查表,路由更新操作相对较少。
- 统计计数器:多个线程累加统计值,偶尔需要读取汇总结果。
但反过来,如果写操作很频繁,读写锁反而可能比互斥锁更慢。因为读写锁的内部实现更复杂,需要维护读者计数、等待队列等数据结构。写操作频繁时,这些开销就得不偿失了。
我的经验:如果写操作占比超过20%,我建议先用互斥锁做基准测试,再决定是否改用读写锁。不要盲目追求「高级」同步机制。
性能对比分析
为了让你更直观地理解性能差异,我画了一张对比图。这张图展示了在不同读写比例下,读写锁和互斥锁的吞吐量表现。
从图中你可以看到:
- 当读操作占比低于30%时,读写锁和互斥锁性能差不多,甚至读写锁略差。
- 当读操作占比超过50%时,读写锁的优势开始显现。
- 当读操作占比达到80%以上时,读写锁的吞吐量可能是互斥锁的3-5倍。
为什么会这样?因为互斥锁不管你是读还是写,一律排队。而读写锁允许读线程并行执行,CPU缓存利用率也更高。我在一个实际项目中测试过,配置表读取场景下,读写锁比互斥锁快了将近4倍。
下面是一个简单的代码示例,展示如何使用 POSIX 读写锁:
#include <pthread.h>
#include <stdio.h>
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
int shared_data = 0;
void* reader(void* arg) {
pthread_rwlock_rdlock(&rwlock);
printf("读取数据: %d\n", shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
void* writer(void* arg) {
pthread_rwlock_wrlock(&rwlock);
shared_data++;
printf("写入数据: %d\n", shared_data);
pthread_rwlock_unlock(&rwlock);
return NULL;
}
int main() {
pthread_t r1, r2, r3, w1;
// 创建三个读线程和一个写线程
pthread_create(&r1, NULL, reader, NULL);
pthread_create(&r2, NULL, reader, NULL);
pthread_create(&r3, NULL, reader, NULL);
pthread_create(&w1, NULL, writer, NULL);
pthread_join(r1, NULL);
pthread_join(r2, NULL);
pthread_join(r3, NULL);
pthread_join(w1, NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
这段代码里,三个读线程可以同时执行,而写线程会等待所有读线程释放锁。反过来,如果写线程先拿到锁,三个读线程都得等着。
小技巧:我习惯在调试时用 pthread_rwlock_tryrdlock 和 pthread_rwlock_trywrlock 来避免死锁。它们不会阻塞,而是立即返回 EBUSY。这在复杂系统中特别有用。
最后说一句,读写锁不是银弹。如果你的场景是「写多读少」,或者读写比例接近1:1,老老实实用互斥锁吧。工具选对了,事半功倍;选错了,事倍功半。
公众号:蓝海资料掘金营,微信deep3321