C++读写锁以及实现方式
文章目录
- 【C++专题】读写锁(Reader-Writer Lock)原理与实现方式(含C++11/20实践)
- 一、读写锁核心概念
- 1. **什么是读写锁?**
- 2. **读写锁 vs 互斥锁**
- 二、C++中的读写锁实现方式
- 方案一:POSIX 读写锁(pthread_rwlock)
- 1. **关键接口**
- 2. **使用示例:缓存读取与更新**
- 3. **注意事项**
- 方案二:C++20 共享互斥锁(std::shared_mutex)
- 1. **核心类**
- 2. **使用示例:线程安全的计数器**
- 3. **优势**
- 方案三:自定义读写锁(基于互斥量与条件变量)
- 1. **实现原理**
- 2. **代码实现(简化版)**
- 3. **关键点**
- 三、读写锁的性能与陷阱
- 1. **性能对比**
- 2. **常见陷阱**
- 四、最佳实践建议
- 五、总结
【C++专题】读写锁(Reader-Writer Lock)原理与实现方式(含C++11/20实践)
一、读写锁核心概念
1. 什么是读写锁?
读写锁是一种同步机制,用于控制多个线程对共享资源的访问,其核心特性:
- 读锁(共享锁):允许多个线程同时获取,适用于只读操作(如读取配置文件、缓存查询)。
- 写锁(排他锁):仅允许单个线程获取,适用于写操作(如修改数据、更新缓存),此时其他线程(包括读线程)均被阻塞。
适用场景:读多写少的场景(如数据库缓存、日志系统),相比普通互斥锁(Mutex)能显著提升并发性。
2. 读写锁 vs 互斥锁
维度 | 互斥锁(Mutex) | 读写锁(Reader-Writer Lock) |
---|---|---|
读操作并发 | 同一时间仅1线程访问 | 允许多线程同时读 |
写操作开销 | 低(简单加锁) | 高(需协调读/写线程) |
典型场景 | 读写均衡或写多读少 | 读多写少(如配置中心、缓存) |
二、C++中的读写锁实现方式
方案一:POSIX 读写锁(pthread_rwlock)
适用范围:Linux/macOS等UNIX-like系统,C++11之前的主流方案。
1. 关键接口
#include <pthread.h>// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);// 加读锁(阻塞式)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 尝试加读锁(非阻塞,成功返回0,失败返回EBUSY)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);// 加写锁(阻塞式)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 尝试加写锁(非阻塞)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);// 销毁锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
2. 使用示例:缓存读取与更新
#include <iostream>
#include <pthread.h>
#include <vector>
#include <thread>pthread_rwlock_t rwlock;
std::vector<int> cache;// 读线程:获取读锁,读取缓存
void read_cache(int id) {pthread_rwlock_rdlock(&rwlock);std::cout << "Thread " << id << " reading: ";for (auto val : cache) {std::cout << val << " ";}std::cout << std::endl;pthread_rwlock_unlock(&rwlock);
}// 写线程:获取写锁,更新缓存
void update_cache(int id, std::vector<int> new_data) {pthread_rwlock_wrlock(&rwlock);cache = new_data;std::cout << "Thread " << id << " updated cache." << std::endl;pthread_rwlock_unlock(&rwlock);
}int main() {pthread_rwlock_init(&rwlock, nullptr);cache = {1, 2, 3};// 启动3个读线程std::thread readers[3];for (int i = 0; i < 3; ++i) {readers[i] = std::thread(read_cache, i);}// 启动1个写线程(会等待读线程释放锁)std::thread writer(update_cache, 4, {4, 5, 6});// 等待所有线程结束for (auto& th : readers) th.join();writer.join();pthread_rwlock_destroy(&rwlock);return 0;
}
3. 注意事项
- 初始化与销毁:需调用
pthread_rwlock_init
和pthread_rwlock_destroy
,避免内存泄漏。 - 线程安全:写锁具有更高优先级,避免读线程饥饿(某些实现支持“写优先”策略)。
方案二:C++20 共享互斥锁(std::shared_mutex)
适用范围:C++20及以上标准,跨平台(Windows/Linux/macOS)。
1. 核心类
std::shared_mutex
:底层实现读写锁语义。std::shared_lock
:用于获取读锁(共享锁)。std::unique_lock
:用于获取写锁(排他锁)。
2. 使用示例:线程安全的计数器
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>std::shared_mutex rw_mutex;
int counter = 0;// 读操作:统计计数器值
void read_counter(int id) {std::shared_lock<std::shared_mutex> lock(rw_mutex); // 自动获取读锁std::cout << "Thread " << id << ": Counter = " << counter << std::endl;
}// 写操作:增加计数器值
void increment_counter(int id, int times) {std::unique_lock<std::shared_mutex> lock(rw_mutex); // 自动获取写锁for (int i = 0; i < times; ++i) {counter++;}std::cout << "Thread " << id << " updated counter to " << counter << std::endl;
}int main() {std::vector<std::thread> threads;// 启动5个读线程for (int i = 0; i < 5; ++i) {threads.emplace_back(read_counter, i);}// 启动2个写线程for (int i = 5; i < 7; ++i) {threads.emplace_back(increment_counter, i, 100);}for (auto& th : threads) th.join();return 0;
}
3. 优势
- RAII风格:通过
std::shared_lock
和std::unique_lock
自动管理锁的生命周期,避免忘记解锁。 - 跨平台兼容:无需依赖POSIX接口,可在Windows(如Visual Studio 2019+)和UNIX系统上编译。
方案三:自定义读写锁(基于互斥量与条件变量)
适用场景:需要理解底层原理,或在不支持C++20的环境中模拟读写锁。
1. 实现原理
- 读计数器:记录当前获取读锁的线程数,无写锁时允许多线程读。
- 写锁标志:表示是否有线程持有写锁,写锁获取时阻塞所有读线程。
- 条件变量:用于写线程等待读线程释放锁,或读线程等待写锁释放。
2. 代码实现(简化版)
#include <mutex>
#include <condition_variable>class ReadWriteLock {
private:std::mutex mtx;std::condition_variable read_cv, write_cv;int reader_count = 0;bool writer_active = false;public:// 获取读锁void lock_read() {std::unique_lock<std::mutex> lock(mtx);// 等待写锁释放read_cv.wait(lock, [this]() { return !writer_active; });reader_count++;}// 释放读锁void unlock_read() {std::unique_lock<std::mutex> lock(mtx);reader_count--;if (reader_count == 0) {write_cv.notify_one(); // 唤醒等待的写线程}}// 获取写锁void lock_write() {std::unique_lock<std::mutex> lock(mtx);// 等待所有读线程释放且无写锁write_cv.wait(lock, [this]() { return reader_count == 0 && !writer_active; });writer_active = true;}// 释放写锁void unlock_write() {std::unique_lock<std::mutex> lock(mtx);writer_active = false;read_cv.notify_all(); // 唤醒所有读线程write_cv.notify_one(); // 唤醒可能等待的写线程}
};
3. 关键点
- 读锁加锁:通过条件变量等待写锁释放,允许多线程同时持有读锁(通过
reader_count
计数)。 - 写锁加锁:必须等待所有读锁释放(
reader_count==0
)且无其他写锁。 - 优先级策略:此实现为“读优先”,写线程可能饥饿,可通过增加写等待标志优化为“写优先”。
三、读写锁的性能与陷阱
1. 性能对比
操作类型 | POSIX读写锁 | std::shared_mutex | 自定义读写锁(读优先) |
---|---|---|---|
读锁加锁耗时 | 低 | 中 | 高 |
写锁加锁耗时 | 中 | 中 | 高 |
适用场景 | 系统级开发 | 现代C++应用 | 学习/定制化需求 |
2. 常见陷阱
- 死锁:读线程持有读锁时尝试获取写锁,或写线程同时获取多个锁(需避免嵌套加锁)。
- 饥饿:读线程过多导致写线程长期无法获取锁(可通过“写优先”策略缓解)。
- 错误解锁:读锁释放时未正确更新计数器,或写锁释放时未通知等待线程。
四、最佳实践建议
- 优先使用C++20标准库:
std::shared_mutex
和std::shared_lock
提供简洁、安全的读写锁实现,推荐用于新项目。 - 明确锁作用域:使用RAII风格的
lock_guard
或unique_lock
,避免锁的作用域泄漏。 - 性能测试:在高并发场景下,通过基准测试(如Google Benchmark)比较读写锁与无锁编程(如原子操作)的性能差异。
- 文档与注释:在代码中注明锁的类型(读锁/写锁)和保护的共享资源,提升可维护性。
五、总结
读写锁是多线程编程中提升读性能的关键工具,其核心在于分离读/写操作的并发策略:
- POSIX读写锁:适用于UNIX系统的高性能场景,但需手动管理锁生命周期。
- C++20共享互斥锁:跨平台、安全、简洁,推荐现代C++项目使用。
- 自定义实现:帮助理解底层原理,但需谨慎处理线程安全和性能问题。
合理使用读写锁能显著优化读多写少场景的并发性,但需注意避免死锁和饥饿问题,结合具体业务场景选择最优方案。
参考资料:
- C++标准文档:std::shared_mutex
- POSIX文档:pthread_rwlock
- 《C++ Concurrency in Action》