m0_73457176头像

深入理解C++ 读写锁(std::shared_mutex)

C++ 读写锁(std::shared_mutex)详解与可运行示例

读写锁解决的是一个非常具体的问题:同一份数据,读的次数远多于写的次数。普通 mutex 在纯读场景下会让所有读者排队,完全浪费了"读不改变数据,本可以并发"这一事实。读写锁就是把这个事实利用起来的机制。


一、模型:读者共享,写者独占

状态读者请求写者请求
无人持锁✅ 立即获得共享锁✅ 立即获得独占锁
有读者持锁✅ 可同时获得共享锁❌ 阻塞,直到所有读者释放
有写者持锁❌ 阻塞,直到写者释放❌ 阻塞

一句话:读读并发,读写互斥,写写互斥。


二、标准库提供的类型

类型标准说明
std::shared_mutexC++17首选,不带超时,实现可更轻量
std::shared_timed_mutexC++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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--