C++ 读写锁(std::shared_mutex)详解与可运行示例
读写锁解决的是一个非常具体的问题:同一份数据,读的次数远多于写的次数。普通 mutex 在纯读场景下会让所有读者排队,完全浪费了"读不改变数据,本可以并发"这一事实。读写锁就是把这个事实利用起来的机制。
一、模型:读者共享,写者独占
| 状态 | 读者请求 | 写者请求 |
|---|---|---|
| 无人持锁 | ✅ 立即获得共享锁 | ✅ 立即获得独占锁 |
| 有读者持锁 | ✅ 可同时获得共享锁 | ❌ 阻塞,直到所有读者释放 |
| 有写者持锁 | ❌ 阻塞,直到写者释放 | ❌ 阻塞 |
一句话:读读并发,读写互斥,写写互斥。
二、标准库提供的类型
| 类型 | 标准 | 说明 |
|---|---|---|
std::shared_mutex | C++17 | 首选,不带超时,实现可更轻量 |
std::shared_timed_mutex | C++14 | 带 try_lock_for/until,C++17 前唯一选择 |
注意:标准库里没有
shared_recursive_mutex,读写锁不支持递归。
配套的锁管理器
| 包装器 | 作用 |
|---|---|
std::shared_lock<M> | 持有共享锁(读),对应 lock_shared() / unlock_shared() |
std::unique_lock<M> | 持有独占锁(写),对应 lock() / unlock() |
std::lock_guard<M> | 也可用于写,但仅限"构造加锁、析构解锁"的简单场景 |
关键规则:
-
读操作用
shared_lock(可以多个线程同时持有); -
写操作用
unique_lock(独占,同一时刻只有一个); -
所有操作走 RAII,不要手写
lock_shared()/unlock_shared()。
三、标准使用范式:封装成线程安全容器
最推荐的用法是把 shared_mutex 藏在一个类里,对外只暴露 get(读)和 set(写):
cpp
class ThreadSafeConfig {
public:
int get(const std::string& key) const {
std::shared_lock<std::shared_mutex> lk(mtx_); // 共享锁
auto it = data_.find(key);
return it == data_.end() ? -1 : it->second;
}
void set(const std::string& key, int value) {
std::unique_lock<std::shared_mutex> lk(mtx_); // 独占锁
data_[key] = value;
}
private:
mutable std::shared_mutex mtx_; // mutable:const 成员函数也要能加锁
std::map<std::string, int> data_;
};
mutable 的作用:get() 是 const 成员函数,但内部要修改 mtx_(加锁会改锁内部状态),所以必须用 mutable 声明锁,否则编译不过。
四、几个必须知道的陷阱
1. 同一线程不能重复加共享锁
shared_mutex 不是递归锁。一个线程已经持有 shared_lock,再对同一个 shared_mutex 加一次 shared_lock,在多数实现(如 glibc 的 pthread_rwlock)上会死锁。标准不保证可重入,实际中几乎都是不可重入。
规避:一个线程内对同一把锁只加一次。需要"内部再读一次"时,把逻辑改为一次拿锁后做完整逻辑。
2. 不支持锁升级 / 降级
标准库没有 shared_lock → unique_lock 的原子升级,也没有反向降级。你只能:
cpp
// 错误:手写升级,中间有窗口期,可能被插队
{
std::shared_lock lk(m);
// 读一读,发现需要写
} // 释放读锁
{
std::unique_lock lk(m);
// 重新加写锁,但此前的假设可能已失效
}
正确做法:
-
如果确定要写,一开始就用
unique_lock; -
或者"写锁下重做整个逻辑"(放弃之前的读结果);
-
或者用其他同步原语(如
atomic配合重试)。
3. 写饥饿
如果读者源源不断,写者可能长时间排不上队。不同实现策略不同(glibc 默认给写者一定优先级,但不保证)。对写延迟敏感时,需要:
-
在应用层控制读者频率;
-
或改用
mutex/ 自研公平锁。
4. 性能误区:临界区太短时可能更慢
shared_mutex 内部维护共享计数、可能需要 futex 等系统调用,单次加锁开销比普通 mutex 大。如果临界区只有"读一个变量"这种极短操作:
-
读者并发带来的收益 < 加锁本身的开销;
-
结果可能
shared_mutex比mutex还慢。
结论:shared_mutex 适合读操作比较"重"(耗时较长、需要计算/遍历/IO)且读远多于写的场景。极短临界区优先考虑 std::atomic 或普通 mutex。
5. 不要在锁内做耗时操作
持共享锁时 sleep、IO、调外部回调,会延长写者的等待;持独占锁做这些则直接把所有读者堵死。锁的持有时间应尽量短。
#include <iostream>
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <vector>
#include <map>
#include <string>
#include <chrono>
#include <atomic>
#include <iomanip>
using namespace std::chrono;
// ---------- 工具 ----------
static steady_clock::time_point g_start = steady_clock::now();
static std::mutex g_cout_mtx;
long now_ms() {
return duration_cast<milliseconds>(steady_clock::now() - g_start).count();
}
void log_line(const std::string& s) {
std::lock_guard<std::mutex> lk(g_cout_mtx);
std::cout << s << "\n";
}
// ============================================================
// 示例 1:ThreadSafeConfig —— shared_mutex 的标准封装
// ============================================================
class ThreadSafeConfig {
public:
int get(const std::string& key) const {
std::shared_lock<std::shared_mutex> lk(mtx_); // 共享(读)
auto it = data_.find(key);
return it == data_.end() ? -1 : it->second;
}
void set(const std::string& key, int value) {
std::unique_lock<std::shared_mutex> lk(mtx_); // 独占(写)
data_[key] = value;
}
std::map<std::string, int> snapshot() const {
std::shared_lock<std::shared_mutex> lk(mtx_);
return data_;
}
private:
mutable std::shared_mutex mtx_; // mutable: const 成员也能加锁
std::map<std::string, int> data_;
};
void demo_api() {
std::cout << "\n===== 示例 1:shared_mutex 基本 API =====\n";
ThreadSafeConfig cfg;
std::thread w([&] {
cfg.set("timeout", 30);
cfg.set("retries", 3);
cfg.set("port", 8080);
});
w.join();
std::vector<std::thread> readers;
for (int i = 0; i < 3; ++i) {
readers.emplace_back([&, i] {
std::cout << "[reader" << i << "] timeout = "
<< cfg.get("timeout") << "\n";
std::cout << "[reader" << i << "] port = "
<< cfg.get("port") << "\n";
});
}
for (auto& t : readers) t.join();
auto snap = cfg.snapshot();
std::cout << "snapshot: ";
for (auto& [k, v] : snap) std::cout << k << "=" << v << " ";
std::cout << "\n";
}
// ============================================================
// 示例 2:可视化并发 / 互斥
// ============================================================
void demo_visualize() {
std::cout << "\n===== 示例 2:可视化并发与互斥 =====\n";
std::cout << "读者持共享锁 100ms,写者持独占锁 150ms\n";
std::shared_mutex rw;
std::atomic<int> readers_in{0};
std::atomic<int> max_concurrent{0};
auto reader = [&](int id) {
std::shared_lock<std::shared_mutex> lk(rw); // 共享锁
int cur = ++readers_in;
int prev = max_concurrent.load();
while (cur > prev &&
!max_concurrent.compare_exchange_weak(prev, cur)) {}
log_line("[r" + std::to_string(id) + "] 进入 t=" +
std::to_string(now_ms()) + "ms (当前并发读者=" +
std::to_string(cur) + ")");
std::this_thread::sleep_for(milliseconds(100)); // 模拟读耗时
log_line("[r" + std::to_string(id) + "] 离开 t=" +
std::to_string(now_ms()) + "ms");
--readers_in;
};
auto writer = [&](int id) {
std::unique_lock<std::shared_mutex> lk(rw); // 独占锁
log_line("[W" + std::to_string(id) + "] 进入 t=" +
std::to_string(now_ms()) + "ms (独占)");
std::this_thread::sleep_for(milliseconds(150)); // 模拟写耗时
log_line("[W" + std::to_string(id) + "] 离开 t=" +
std::to_string(now_ms()) + "ms");
};
// ---- 阶段 1:3 个读者同时启动 → 读读并发 ----
std::cout << "\n--- 阶段 1:3 个读者并发 ---\n";
{
std::vector<std::thread> ts;
for (int i = 1; i <= 3; ++i) ts.emplace_back(reader, i);
for (auto& t : ts) t.join();
}
log_line("最大并发读者数 = " + std::to_string(max_concurrent.load()));
// ---- 阶段 2a:读者持锁时写者请求 → 写者等待 ----
std::cout << "\n--- 阶段 2a:读者先持锁,写者随后请求 → 写者等 ---\n";
readers_in = 0;
max_concurrent = 0;
{
std::thread r1(reader, 4);
std::thread r2(reader, 5);
std::this_thread::sleep_for(milliseconds(20)); // 确保读者先持锁
std::thread w(writer, 1);
r1.join(); r2.join(); w.join();
}
// ---- 阶段 2b:写者持锁时读者请求 → 读者等待 ----
std::cout << "\n--- 阶段 2b:写者先持锁,读者随后请求 → 读者等 ---\n";
readers_in = 0;
max_concurrent = 0;
{
std::thread w(writer, 2);
std::this_thread::sleep_for(milliseconds(20)); // 确保写者先持锁
std::thread r1(reader, 6);
std::thread r2(reader, 7);
w.join(); r1.join(); r2.join();
}
}
// ============================================================
// 示例 3:mutex vs shared_mutex 纯读吞吐
// ============================================================
template <typename Mutex, template <typename> class Lock>
long bench_read(const char* name, int nthreads, int iters, int& data) {
Mutex m;
auto start = steady_clock::now();
std::vector<std::thread> ts;
for (int i = 0; i < nthreads; ++i) {
ts.emplace_back([&] {
for (int j = 0; j < iters; ++j) {
Lock<Mutex> lk(m);
volatile int x = data; // 极短临界区:只读一个变量
(void)x;
}
});
}
for (auto& t : ts) t.join();
long ms = duration_cast<milliseconds>(
steady_clock::now() - start).count();
std::cout << " " << name << ": " << ms << "ms\n";
return ms;
}
void demo_bench() {
std::cout << "\n===== 示例 3:mutex vs shared_mutex(纯读、极短临界区)=====\n";
const int T = 4, N = 200000;
int data = 42;
bench_read<std::mutex, std::lock_guard>("mutex ", T, N, data);
bench_read<std::shared_mutex, std::shared_lock>("shared_mutex", T, N, data);
std::cout << " 提示:临界区极短时 shared_mutex 不一定更快(见文末说明)\n";
}
int main() {
demo_api();
demo_visualize();
demo_bench();
return 0;
}
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/m0_73457176/article/details/169518859



