在多线程程序中,读多写少的场景非常常见。传统的互斥锁(std::mutex)只能保证同一时间只有一个线程访问共享资源,无论是读还是写,这导致读操作被不必要的阻塞。C++17引入的 std::shared_mutex 解决了这一问题,它允许多个线程同时读共享资源,但对写操作进行独占访问。下面我们从概念、使用方法、性能优化以及常见错误四个方面展开讨论,帮助你在项目中更高效地使用读写锁。
1. 读写锁的基本概念
- 共享锁(shared lock):
std::shared_lock或者std::shared_mutex::lock_shared(),可由多个线程同时持有,适用于只读操作。 - 独占锁(exclusive lock):
std::unique_lock或者std::shared_mutex::lock(),只允许单个线程持有,适用于写操作。 - 优先级:标准库没有规定读或写的优先级;如果大量读线程持续持有共享锁,写线程可能会饥饿。需要根据业务需求手动调整。
2. 基本使用示例
#include <shared_mutex>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>
class SharedData {
public:
void setValue(int v) {
std::unique_lock lock(mtx_); // 独占锁
data_ = v;
}
int getValue() const {
std::shared_lock lock(mtx_); // 共享锁
return data_;
}
private:
mutable std::shared_mutex mtx_;
int data_ = 0;
};
void writer(SharedData& d, int id) {
for (int i = 0; i < 10; ++i) {
d.setValue(i);
std::cout << "[Writer " << id << "] wrote " << i << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
void reader(const SharedData& d, int id) {
for (int i = 0; i < 20; ++i) {
int v = d.getValue();
std::cout << "[Reader " << id << "] read " << v << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
}
int main() {
SharedData d;
std::thread w1(writer, std::ref(d), 1);
std::thread w2(writer, std::ref(d), 2);
std::vector<std::thread> readers;
for (int i = 0; i < 3; ++i)
readers.emplace_back(reader, std::cref(d), i+1);
w1.join(); w2.join();
for (auto& t : readers) t.join();
}
- 注意:
SharedData::getValue()的锁对象声明为mutable,因为getValue()本身是const,但需要获取锁。
3. 性能优化技巧
| 场景 | 优化手段 | 说明 |
|---|---|---|
| 读多写少 | 使用 std::shared_mutex 代替 std::mutex |
允许并发读 |
| 读多写少 | 尽量把读操作放在临界区外 | 只锁住真正需要保护的数据 |
| 写操作频繁 | 采用“读写分离”策略:写时把数据复制到临时结构,再一次性交换 | 减少锁持有时间 |
| 写线程可能饥饿 | 结合 std::shared_timed_mutex 的 try_lock() 与睡眠重试 |
让写线程有机会抢占锁 |
| 需要动态调整读写比例 | 在运行时根据线程数量动态切换使用 std::mutex 或 std::shared_mutex |
适配不同负载 |
4. 常见错误与解决方案
| 错误 | 说明 | 解决方案 |
|---|---|---|
| 读线程持锁时间过长 | 读线程在共享锁下执行复杂计算导致写线程阻塞 | 把计算放到锁外,或者使用读写分离结构 |
| 写线程频繁竞争 | 同时有多个写线程竞争独占锁导致延迟 | 采用事务式写,或者使用锁排队策略 |
| 死锁 | 在同一函数中先持共享锁再尝试独占锁,或循环依赖 | 避免锁嵌套;使用 std::lock 或 std::scoped_lock |
| 数据竞争 | 忘记使用共享锁或独占锁 | 在所有访问点加锁,或者使用 std::atomic |
5. 读写锁的实际应用场景
- 缓存系统:大量线程读取缓存,写线程只在缓存失效或更新时触发。
- 配置管理:应用启动后多线程读取配置文件,只有管理员线程修改。
- 日志系统:读线程需要读取日志内容做统计,写线程负责追加日志。
6. 小结
std::shared_mutex为读多写少的并发场景提供了天然的并发读优势。- 正确的锁粒度与锁时机是提升性能的关键。
- 结合业务特性(读写比例、写线程饥饿等)灵活切换锁策略。
通过合理使用读写锁,你可以在保持数据一致性的前提下,显著提升多线程程序的吞吐量和响应速度。祝你编码愉快!