我见过最慢的一种慢。
不是算法慢。
是大家都在等。
线程一堆。
CPU 不怎么忙。
但请求就是卡。
你去看火焰图。
全在锁上。
当年:一个 mutex 解决一切,然后它就成了瓶颈
最朴素的写法是。
#include <mutex>
std::mutex m;
void read() {
std::lock_guard<std::mutex> lk(m);
// read shared data
}
它很安全。
也很容易写对。
但它把“读”和“写”一视同仁。
只要有人在读。
别人也得等。
线上啪一下:缓存是读多写少,但我们用同一把锁把所有读者堵住了
假设你写了个小服务。
有个配置缓存。
99% 是读。
1% 才更新一次。
你用 mutex。
所有请求都会排队读配置。
线上表现就是。
“明明没压力,延迟却在抖”。
C++17:shared_mutex,让读者可以同时进来
std::shared_mutex 可以先这样理解。
写的时候独占。
读的时候共享。
#include <shared_mutex>
std::shared_mutex mu;
void read_config() {
std::shared_lock<std::shared_mutex> lk(mu);
// read
}
void update_config() {
std::unique_lock<std::shared_mutex> lk(mu);
// write
}
shared_lock 允许多个读线程同时持有。
写线程会等到所有读者离开。
读者也会等写者结束。
这非常适合读多写少。
另一个老坑:锁两把 mutex,死锁就会自己找上门
你写并发久了。
迟早会写到“同时改两份数据”。
比如转账。
std::mutex a;
std::mutex b;
void transfer() {
std::lock_guard<std::mutex> lk1(a);
std::lock_guard<std::mutex> lk2(b);
}
如果另一个线程反过来先锁 b 再锁 a。
两边就会互相等。
这就是死锁。
线上就是。
“突然卡死,重启才好”。
C++17:scoped_lock,一次把多把锁锁住
#include <mutex>
std::mutex a;
std::mutex b;
void transfer() {
std::scoped_lock lk(a, b);
// safe update both
}
scoped_lock 会用更稳的方式去拿锁。
它的目的很明确。
帮你避开常见的锁顺序死锁。
关键结论
并发里最贵的不是算。
是等待。
小结:读多写少用 shared_mutex,多锁用 scoped_lock
当年我们用一把 mutex 打天下。
能跑。
但很容易在规模上来后变瓶颈。
C++17 给的两个工具都很务实。
一个让读者别互相挡路。
一个让你少写一次死锁复盘报告。