std::shared_mutex 及其用法
std::shared_mutex 是 C++17 引入的读写锁(共享互斥锁),专门用于读多写少场景。
普通 std::mutex 是独占锁:不管读还是写,同一时间只允许一个线程访问。
shared_mutex 支持两种模式:
- 共享模式(读锁):多个线程可以同时获取读锁,并发读;
- 独占模式(写锁):只有一个线程拿到写锁,写的时候所有读、写线程全部阻塞。
C++14 有std::shared_timed_mutex,带超时;C++17std::shared_mutex去掉超时,性能更好。
核心接口
表格
| 接口 | 作用 |
|---|---|
lock() |
独占上锁(写),阻塞;其他读 / 写全部等待 |
unlock() |
释放独占锁 |
try_lock() |
尝试独占上锁,拿不到直接返回 false |
lock_shared() |
共享上锁(读),多个线程可同时成功 |
unlock_shared() |
释放共享锁 |
try_lock_shared() |
尝试获取读锁 |
⚠️注意:
- 写锁优先级:一旦有线程等待写锁,后续新来的
lock_shared()会被阻挡,避免写线程永久饥饿;- 不能同一个线程重复调用
lock_shared(),会死锁;- 不要混用:
lock_shared()之后不能调用unlock(),必须配对unlock_shared()。
RAII 包装(推荐,不要裸调用 lock)
C++ 提供两个 RAII 锁管理器配合 shared_mutex:
- std::shared_lockstd::shared_mutex → 获取共享读锁,构造调用
lock_shared(),析构自动unlock_shared() - std::unique_lockstd::shared_mutex → 获取独占写锁,构造调用
lock(),析构自动unlock()
✅业务代码几乎永远用 RAII,不要手动lock/unlock,防止异常导致死锁。
完整示例代码
cpp
运行
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
std::shared_mutex rw_mtx;
int shared_data = 0;
// 读线程:共享锁,多线程可并发执行
void reader(int id) {
for (int i = 0; i < 3; ++i) {
// 获取读锁,多个reader可以同时持有
std::shared_lock<std::shared_mutex> lock(rw_mtx);
std::cout << "reader" << id << ", data = " << shared_data << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
// 写线程:独占锁,同一时间只能一个写
void writer() {
for (int i = 0; i < 3; ++i) {
// 获取写锁,排他,所有读/写阻塞
std::unique_lock<std::shared_mutex> lock(rw_mtx);
shared_data++;
std::cout << "writer update data = " << shared_data << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
int main() {
std::vector<std::thread> threads;
// 4个读线程
for(int i=0;i<4;i++){
threads.emplace_back(reader, i);
}
// 1个写线程
threads.emplace_back(writer);
for(auto& t : threads) t.join();
return 0;
}
执行行为
- 4 个 reader 可以同时打印,并发读;
- writer 拿到锁瞬间,所有 reader 全部卡住;writer 执行完释放锁,读线程恢复并发。
使用场景 & 不适合场景
✅适合:读多写少
- 配置表、缓存、全局只读数据,偶尔更新;
- 大量线程查询,少量线程修改。
❌不适合:
- 读写频率差不多:读写锁有额外开销,不如普通
std::mutex; - 写极频繁:写会不断阻塞读,性能反而差;
- 递归加锁:
shared_mutex不支持递归,同一个线程重复 lock 会死锁;需要递归读写锁自己封装。
易错坑点
- 锁类型配对错误
cpp
运行
std::shared_lock<...> lk(rw_mtx); rw_mtx.unlock(); // ❌错误,应该 unlock_shared(),RAII不会犯这个错
- 持有读锁的时候去做写操作
拿到shared_lock后,千万不要修改共享变量;此时其他线程也拿着读锁,会数据竞争。
- 死锁:读锁升级写锁(不支持)
cpp
运行
std::shared_lock<std::shared_mutex> lk(rw_mtx); // rw_mtx.lock(); // ❌ 已经持有共享锁,再申请独占锁直接死锁!
读锁不能直接升级写锁。正确做法:释放读锁,再获取写锁;或者先 try 升级,注意数据已经被别的线程修改。
- 写锁降级(C++17 支持)
已经拿到
unique_lock写锁,可以释放独占,转为共享读锁。
cpp
运行
std::unique_lock<std::shared_mutex> wlock(rw_mtx); // 写操作... wlock.unlock(); std::shared_lock<std::shared_mutex> rlock(rw_mtx);
和其他锁对比
表格
| 锁 | 特性 | 适用场景 |
|---|---|---|
std::mutex |
独占锁,简单快速 | 读写均衡,写多 |
std::shared_mutex |
读写锁 C++17,无超时 | 读多写少 |
std::shared_timed_mutex |
读写锁 + 超时 C++14 | 需要 try_lock 带超时 |
C++20 补充
C++20 对
std::shared_mutex增加了try_lock_shared_for这类超时接口;同时支持 constexpr。简单总结记忆
shared_mutex= 读写锁;- 读 → std::shared_lock;
- 写 → std::unique_lock;
- 读锁不能升级写锁,会直接死锁;
- 读多写少才用,否则直接 mutex。
如果你需要,我可以给一段生产风格的封装示例(线程安全缓存类,基于 shared_mutex)。
