You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

std::shared_mutex问题:首次多线程可并发读,二次读需依次等待?

问题分析与解决

为什么第二次Get()会串行执行?

咱们拆解一下整个流程的锁机制和线程交互逻辑:

1. 第一次Get()能并行的原因

所有线程几乎同时启动,第一时间调用Get()获取共享锁(std::shared_lock)。std::shared_mutex的核心规则就是允许多个读者同时持有共享锁,所以10个线程能同时拿到锁,齐刷刷输出初始值0,然后一起Sleep1秒,最后同步释放锁——这就是你看到第一次读取并行的原因。

2. 第二次Get()串行的核心问题

当所有线程完成第一次读取后,都会去竞争Increase()的独占锁(std::unique_lock)。这里的关键坑点来了:

  • 第一个线程抢到独占锁,完成value++后主动释放锁,紧接着就进入第二次Get(),又拿到了共享锁,然后开始Sleep1秒。
  • 而shared_mutex有个硬性规则:只要有线程拿着共享锁,任何需要独占锁的操作(比如Increase())都无法获取锁。所以后面的线程想要执行Increase(),必须等当前拿着共享锁的线程把第二次Get()做完、释放锁才行。

这样就形成了链式阻塞:线程A做完第二次Get()→线程B才能抢独占锁做Increase()→线程B做完Increase()又去拿共享锁做第二次Get()→线程C继续等……循环往复,最终第二次Get()只能一个接一个执行,每个间隔1秒。


解决方法

根据你的需求不同,有两种方案可选:

方案1:让第二次Get()并行读取最终值

如果你希望所有线程完成写操作后,同时读取最终的计数器结果,可以用同步屏障(std::barrier,需要C++20及以上)来协调线程顺序,确保所有线程都写完了再一起读:

修改后的代码:

#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <thread>
#include <windows.h>
#include <vector>
#include <barrier>

using namespace std;

class Counter {
public:
    Counter() : value_(0) { }
    std::size_t Get() const {
        std::shared_lock<std::shared_mutex> lock(mutex_);
        std::cout << std::this_thread::get_id() << ' ' << value_ << std::endl;
        Sleep(1000);
        return value_;
    }
    void Increase() {
        // 用lock_guard自动管理锁,比手动unlock更简洁安全
        std::lock_guard<std::shared_mutex> lock(mutex_);
        value_++;
    }
private:
    mutable std::shared_mutex mutex_;
    std::size_t value_;
};

void Worker(Counter& counter, std::barrier<>& barrier_after_read, std::barrier<>& barrier_after_write) {
    // 第一步:所有线程并行读初始值
    counter.Get();
    barrier_after_read.arrive_and_wait(); // 等所有线程读完再往下走

    // 第二步:所有线程串行写(没办法,独占锁只能串行)
    counter.Increase();
    barrier_after_write.arrive_and_wait(); // 等所有线程写完再读

    // 第三步:所有线程并行读最终值
    counter.Get();
}

int main() {
    constexpr int num_threads = 10;
    Counter counter;
    std::barrier barrier_after_read(num_threads);
    std::barrier barrier_after_write(num_threads);
    std::vector<std::thread> v;

    for(int i = 0; i < num_threads; i++){
        v.emplace_back(&Worker, std::ref(counter), std::ref(barrier_after_read), std::ref(barrier_after_write));
    }
    for (std::thread& t : v) t.join();

    return 0;
}

运行效果:

  • 先一次性输出10个0,Sleep1秒后释放锁;
  • 所有线程快速串行完成写操作(瞬间完成);
  • 最后一次性输出10个10,Sleep1秒后结束。

方案2:接受串行读取(如果需要中间值)

如果你的需求就是每个线程读取自己Increase()后的中间值,那这种串行现象是无法避免的——毕竟写操作必须串行,且第二次读会阻塞后续的写。不过可以优化一下Increase()的实现,把手动unlock()换成lock_guard,代码更简洁安全:

void Increase() {
    std::lock_guard<std::shared_mutex> lock(mutex_);
    value_++;
}

内容的提问来源于stack exchange,提问作者scirocc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 13:38:17