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
相关产品推荐
相关产品推荐

