Appearance
死锁 / 线程安全单例 / 锁选型
一、死锁(Deadlock)
产生:四个必要条件(同时具备)
- 互斥:资源一次只能被一个线程占用。
- 持有并等待:持有一资源同时等待另一资源。
- 不可剥夺:资源不能被强制夺走,只能自愿释放。
- 循环等待:若干线程形成等待环(A 等 B 持的,B 等 A 持的)。
经典例子:
cpp
// 两把锁顺序不一致 → 死锁
std::mutex a, b;
void f1() { std::lock_guard lk1(a); std::lock_guard lk2(b); /*...*/ }
void f2() { std::lock_guard lk1(b); std::lock_guard lk2(a); /*...*/ }
// f1 拿 a 等 b,f2 拿 b 等 a → 卡死避免方法
- 统一加锁顺序(所有线程按相同顺序 a→b 拿锁)。最实用。
- 用
std::lock(a, b)(C++11)一次锁多把避免死锁(内部做无死锁的 try 排序)。 - 减少锁粒度 / 尽量单锁。
- 锁超时(try_lock_for) + 回退(不常用于裸 mutex,但可在设计上)。
- 设计避免嵌套持锁(锁中不再锁别的)。
二、std::lock 一次锁多个
cpp
std::lock(a, b); // 无死锁地同时锁 a/b
std::lock_guard<std::mutex> lk1(a, std::adopt_lock); // 已持有不在二次 lock
std::lock_guard<std::mutex> lk2(b, std::adopt_lock);三、线程安全单例(Meyers Singleton / 三次代表性写法)
写法 1:局部 static(C++11 起线程安全,最推荐)
cpp
class Singleton {
public:
static Singleton& instance() {
static Singleton inst; // C++11 保证首次初始化线程安全
return inst;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
};编译器会在首次 instance() 里插入 guard 变量 + 互斥;简单、延迟初始化、无死锁风险。
写法 2:call_once
cpp
class Singleton {
static Singleton* inst;
static std::once_flag flag;
public:
static Singleton& instance() {
std::call_once(flag, []{ inst = new Singleton; });
return *inst;
}
};
Singleton* Singleton::inst = nullptr;
std::once_flag Singleton::flag;写法 3:双检锁 DCLP(考察原子/内存序,易错)
cpp
class Singleton {
static std::atomic<Singleton*> inst;
public:
static Singleton& instance() {
Singleton* p = inst.load(std::memory_order_acquire);
if (!p) { // 慢路径
std::lock_guard<std::mutex> lk(mu);
p = inst.load(std::memory_order_relaxed);
if (!p) { // 二次检查
p = new Singleton;
inst.store(p, std::memory_order_release); // 发布
}
}
return *p;
}
};旧版 bug:不加内存序/换成普通指针时,可能另一个线程拿到「半初始化」的指针(reorder)。现代应使用 atomic + acquire/release,或直接局部 static(最省事)。
四、锁的选型小结
| 场景 | 方案 |
|---|---|
| 普通互斥保护 | std::mutex + lock_guard |
| 需要临时解锁/可移动/配合条件变量 | std::unique_lock |
| 读多写少 | std::shared_mutex(shared 读锁 / unique 写锁) |
| 只为自旋短临界区 | std::atomic_flag 自旋 |
| 热路径原子计数/标志 | std::atomic |
| 一次性初始化 | 局部 static / call_once |
五、易错坑
- 忘记 RAII(手工 lock 忘 unlock)。
- 锁里调用会再拿同一把锁的重入函数 → std::mutex 非递归,死锁(用
std::recursive_mutex或避免重入)。 - 锁 + 条件变量未用 same mutex。
- 析构次序 / static 的 Fiasco:跨翻译单元 static 初始化顺序未定义,用局部 static(函数内)规避。