Skip to content

死锁 / 线程安全单例 / 锁选型

一、死锁(Deadlock)

产生:四个必要条件(同时具备)

  1. 互斥:资源一次只能被一个线程占用。
  2. 持有并等待:持有一资源同时等待另一资源。
  3. 不可剥夺:资源不能被强制夺走,只能自愿释放。
  4. 循环等待:若干线程形成等待环(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

五、易错坑

  1. 忘记 RAII(手工 lock 忘 unlock)。
  2. 锁里调用会再拿同一把锁的重入函数 → std::mutex 非递归,死锁(用 std::recursive_mutex 或避免重入)。
  3. 锁 + 条件变量未用 same mutex。
  4. 析构次序 / static 的 Fiasco:跨翻译单元 static 初始化顺序未定义,用局部 static(函数内)规避。

C++ 面试八股 · VitePress 版