std::shared_mutex¶
| 标准 | 难度 | 状态 | 分级 | 证据 | 审阅 | 最后校验 |
|---|---|---|---|---|---|---|
| C++17 | 3/5 | final | A | 3 条(manual 3) | 单审(zyuhao7) | 2026-10-25 |
编译器支持:GCC 6+ · Clang 3.7+ · MSVC 19.0+。版本查询:cppreference 编译器支持。
主张与证据¶
| 主张 | 支持证据 |
|---|---|
| 读路径用 shared_lock 快速命中,未命中再升级为 unique_lock 创建并二次检查 | ev-mold-shared-mutex-output-section-001 |
| 并行任务在 shared_lock 下复制共享映射,再在线程本地缓存上工作 | ev-mold-shared-mutex-thread-cache-002 |
| 发布路径用 shared_lock 读取订阅表快照,再在锁外分发消息 | ev-drogon-shared-mutex-pubsub-003 |
本站证据页:ev-drogon-shared-mutex-pubsub-003 · ev-mold-shared-mutex-output-section-001 · ev-mold-shared-mutex-thread-cache-002
解决什么问题¶
有些共享数据读多写少:符号表、路由表、配置缓存。用 std::mutex 保护时,多个只读查询也被迫串行,吞吐量随读者数量下降。用普通读写锁又常引入平台特定 API 或额外依赖。
std::shared_mutex 提供两种所有权:多个线程可以同时持有共享锁,但共享锁与独占锁互斥。读路径用 std::shared_lock,写路径用 std::unique_lock(或 std::scoped_lock),从而让并发读真正并行。
基础用法¶
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <string>
#include <unordered_map>
#include <utility>
class ConfigCache {
public:
std::optional<std::string> get(const std::string& key) const {
std::shared_lock lock(mu_);
if (auto it = values_.find(key); it != values_.end())
return it->second;
return std::nullopt;
}
void set(std::string key, std::string value) {
std::unique_lock lock(mu_);
values_[std::move(key)] = std::move(value);
}
private:
mutable std::shared_mutex mu_;
std::unordered_map<std::string, std::string> values_;
};
读操作只持有共享锁,多个线程可以同时执行 get;set 获取独占锁后会等待所有读者退出。mu_ 加 mutable 是因为 get 是 const 成员函数,但仍需锁定互斥量。
进阶用法¶
最常见的进阶写法是“共享锁快速命中,未命中再升级为独占锁”,因为标准没有原子的锁升级操作,必须释放共享锁后重新获取独占锁,并在拿到写锁后二次检查。
std::string get_or_create(const std::string& key) {
{
std::shared_lock lock(mu_);
if (auto it = values_.find(key); it != values_.end())
return it->second;
}
std::unique_lock lock(mu_);
auto [it, inserted] = values_.try_emplace(key, key + "-value");
return it->second;
}
try_emplace 在写锁内二次检查,避免两个线程都未命中后重复创建。锁对象用 CTAD 写成 std::shared_lock lock(mu_),模板参数由构造函数推导。
另一个方向是“读锁内只取快照,锁外处理”。读者在共享锁下复制 shared_ptr 或容器,释放锁后再遍历;这样慢操作不会阻塞写者,代价是快照可能短暂落后于最新状态。
完整示例见 examples/cpp17/shared_mutex.cpp。
真实项目证据¶
| 项目 | 位置 | 用法 | 为什么典型 |
|---|---|---|---|
| rui314/mold | src/output-chunks.cc#L2075-L2089 |
共享锁查找输出段,未命中后用独占锁创建并二次检查 | 读多写少;拿到写锁后重新查找,避免并发插入重复对象 |
| rui314/mold | src/passes.cc#L571-L578 |
并行任务在共享锁下复制映射,再使用线程本地副本 | 临界区只做拷贝,后续查询不持锁;注释明确说明为降低锁竞争 |
| Drogon | lib/inc/drogon/PubSubService.h#L51-L57 |
发布路径用共享锁读取订阅表快照,锁外分发消息 | 回调可能很慢,不能持锁执行;快照复制把临界区压到最短 |
三条证据均为 manual 级:作者复核,尚未引入第二名审核者。mold 与 Drogon 都把共享锁限制在快照复制或查找阶段,写锁路径再单独处理。
陷阱与边界¶
读锁不保证数据不变。 std::shared_lock 只排斥写者;若多个读者同时修改同一数据,仍然存在数据竞争。共享锁适合“只读访问”,不适合“多个线程都写但希望并发”。
没有原子的升级操作。 不能把已持有的 shared_lock 直接提升为独占锁。需要写入时,先释放共享锁,再获取 unique_lock,并在获得写锁后重新检查条件:
{
std::shared_lock read_lock(mu);
if (contains(key)) return;
}
std::unique_lock write_lock(mu);
if (contains(key)) return;
insert(key);
两段检查之间数据可能变化,第二次检查不可省略。
写者可能饥饿。 标准不规定读者与写者的优先级;读请求持续到来时,写线程可能长时间拿不到锁。写操作频繁或延迟敏感时,用公平性更好的实现,或在应用层限制读并发。
临界区要短。 读写锁的加解锁比普通 mutex 更重;只有读操作确实占多数、且临界区有一定工作量时才划算。单纯读一个 int 用 std::atomic 更便宜。
不要递归加锁。 shared_mutex 不是递归互斥量;同一线程重复获取锁(无论共享还是独占)都可能死锁。需要递归语义时重新设计调用边界。
生命周期仍然由调用方保证。 shared_lock、unique_lock 都只借用互斥量;锁对象不能比 shared_mutex 活得更久。
条件变量需要区分类型。 std::condition_variable 只接受 std::unique_lock<std::mutex>;std::condition_variable_any 可以配合 std::shared_lock 和 std::unique_lock 使用,但等待/通知场景仍要重新检查条件,读多写少的缓存通常不需要它。不要为了读并发把普通等待队列强行换成读写锁。
库联动¶
- mold:输出段缓存用共享锁命中、独占锁创建;并行任务在共享锁下复制映射后改用线程本地缓存。
- Drogon:发布订阅服务在共享锁下复制处理器表,锁外执行用户回调;同一类型用特性宏在
shared_mutex与shared_timed_mutex间切换。 - 与
std::atomic<std::shared_ptr<T>>的分工:读写锁适合“原地修改共享容器”;如果写操作总是整体替换只读快照,C++20 的原子shared_ptr可以完全去掉读锁。
横向对比¶
| 方案 | 并发读 | 写独占 | 主要代价 |
|---|---|---|---|
std::shared_mutex |
是 | 是 | 比 mutex 重;写者可能饥饿 |
std::mutex |
否 | 是 | 简单、开销低;读多写少时吞吐受限 |
std::shared_timed_mutex |
是 | 是 | 支持超时;实现更重 |
std::atomic<T> |
是 | 是(单值) | 只适合单个可原子操作的值 |
| 线程本地缓存 + 定期发布 | 是 | 少量 | 需要处理快照一致性 |
跨标准演进:std::shared_timed_mutex 在 C++14 引入,std::shared_mutex 在 C++17 通过 N4508 加入,去掉了超时接口以获得更轻的实现。MSVC 从 19.0 Update 2(VS 2015 Update 2)起提供 <shared_mutex>;旧工具集上不能只按 _MSC_VER >= 1900 判断。C++20 的 std::atomic<std::shared_ptr<T>> 为“整体替换快照”提供了另一条路径,适合读多写少且写操作只做整体替换的场景。