一、死锁四大必要条件(缺一不可)

死锁的发生必须同时满足以下四个核心条件,只要打破其中任意一个,就能避免死锁产生。

 1. 互斥条件(Mutual Exclusion)

核心定义:资源具有排他性,同一时间仅能被一个进程或线程独占使用,其他试图获取该资源的进程必须等待,直到资源被释放。这是最基础的资源特性,也是死锁发生的前提之一,比如打印机、独享的 I/O 设备、数据库写锁等,都具备典型的互斥属性。

代码示例:

cpp

#include <mutex>
std::mutex mutex; // 互斥资源
void use_resource() {
    mutex.lock();  // 独占获取
    // 资源使用逻辑
    mutex.unlock();// 释放资源
}

 2. 占有并请求(Hold and Wait)

核心定义:进程已经持有至少一个资源,却又向系统请求新的资源,而新资源恰好被其他进程占用,此时该进程不会释放已持有的资源,而是进入阻塞状态等待新资源。这种 “占着不放又要新的” 行为,是死锁形成的关键环节。例如进程 P1 持有锁 A,又请求被 P2 占有的锁 B,同时 P2 持有锁 B 又请求 P1 的锁 A,双方都会陷入等待。

代码示例:

cpp

#include <mutex>
std::mutex A, B;
void process() {
    std::lock_guard<std::mutex> lockA(A); // 持有A
    std::lock_guard<std::mutex> lockB(B); // 请求B(可能阻塞)
}

 3. 不可剥夺(No Preemption)

核心定义:已分配给进程的资源不能被系统强制收回,只能由持有资源的进程主动释放。即使持有资源的进程陷入阻塞,系统也无法剥夺其资源分配给其他进程,这使得等待状态无法被外力打破。比如一个进程获取了锁后崩溃且未主动释放,若系统无法强制回收锁,其他进程将永远等待该锁,进而引发死锁。

 4. 循环等待(Circular Wait)

核心定义:多个进程形成首尾相连的环形等待链,每个进程都等待链中下一个进程持有的资源,且等待关系无任何出口。这种闭环结构让所有进程都陷入 “等别人释放,别人也等我释放” 的死循环,是死锁的最终表现形式。例如进程 1 等进程 2 的资源、进程 2 等进程 3 的资源、进程 3 等进程 1 的资源,三者形成闭环。

代码示例:

cpp

#include <mutex>
#include <thread>
std::mutex mA, mB, mC;
void t1() { std::lock_guard<std::mutex> l(mA); std::lock_guard<std::mutex> l(mB); }
void t2() { std::lock_guard<std::mutex> l(mB); std::lock_guard<std::mutex> l(mC); }
void t3() { std::lock_guard<std::mutex> l(mC); std::lock_guard<std::mutex> l(mA); }
void deadlock() {
    std::thread th1(t1), th2(t2), th3(t3);
    th1.join(); th2.join(); th3.join();
}

二、死锁核心成因

死锁本质是 “资源分配” 与 “进程执行” 失衡导致的结果,核心成因可分为三类:

  1. 系统资源不足:CPU、内存、锁、I/O 设备等稀缺资源的总量无法满足所有并发进程的需求,进程为争夺有限资源必然产生竞争,这是死锁发生的根本原因。
  2. 进程推进顺序不当:进程请求和释放资源的时序不合理,比如部分进程先占资源再请求,部分进程无序请求资源,最终触发持有并请求、循环等待等死锁条件。
  3. 资源分配策略缺陷:静态分配(进程启动前分配所有所需资源)会导致资源利用率极低,且易造成资源闲置;动态分配(进程运行中按需分配)若时机或规则错误,会加剧资源竞争,增加死锁概率。

三、死锁检测方法

死锁检测的核心目标是及时发现系统中的死锁状态,为后续恢复提供依据,常用方法有两种:

🔍 1. 资源分配图检测

核心原理:构建 “进程 - 资源” 有向图,图中节点分为进程节点和资源节点,边分为 “进程请求资源” 的请求边、“资源分配给进程” 的分配边。若检测到图中存在闭环,则判定系统发生死锁。这种方法是死锁检测的理论核心,能精准定位死锁的进程和资源链。

代码示例:

cpp

#include <unordered_map>
#include <unordered_set>
#include <vector>

struct Graph {
    std::unordered_map<int, std::vector<int>> proc2res; // 进程持有的资源
    std::unordered_map<int, std::vector<int>> res2proc; // 资源被哪些进程等待
};

class Detector {
private:
    bool dfs(int p, std::unordered_set<int>& vis, std::unordered_set<int>& stack, const Graph& g) {
        vis.insert(p); stack.insert(p);
        for (int r : g.proc2res[p])
            for (int np : g.res2proc[r])
                if (!vis.count(np) && dfs(np, vis, stack, g) || stack.count(np)) return true;
        stack.erase(p);
        return false;
    }
public:
    bool is_deadlock(const Graph& g) {
        std::unordered_set<int> vis, stack;
        for (auto& [p, _] : g.proc2res)
            if (!vis.count(p) && dfs(p, vis, stack, g)) return true;
        return false;
    }
};

🔍 2. 超时检测

核心原理:这是工程中最常用的简易检测手段,为资源获取(如加锁)设置超时阈值,若进程在指定时间内未获取到资源,则判定存在死锁风险。该方法无需复杂的图分析,实现简单,虽可能出现 “误判”(如资源只是暂时繁忙),但实用性强,适合大多数场景。

代码示例:

cpp

#include <chrono>
#include <mutex>
bool try_lock(std::timed_mutex& m, int ms = 100) {
    return m.try_lock_for(std::chrono::milliseconds(ms)); // 超时返回false
}

四、死锁恢复策略

死锁恢复是死锁发生后的补救措施,核心思路是打破死锁的必要条件,释放被占用的资源,恢复系统正常运行,常用策略如下:

🔄 1. 重新启动

核心逻辑:通过重启整个系统或死锁相关进程,强制释放所有被占用的资源,彻底打破死锁。这是最简单的恢复方式,无需复杂的资源分析和处理,但代价极高 —— 重启会导致所有进程(包括非死锁进程)的运行状态和未持久化数据丢失,服务中断时间长。仅适用于嵌入式系统、简单单机程序等对数据和服务连续性要求低的场景。

代码示例:

cpp

#include <cstdlib>
void reboot() {
#ifdef _WIN32
    std::system("shutdown /r /t 0"); // Windows重启
#else
    std::system("reboot"); // Linux重启
#endif
}

🔄 2. 终止进程

核心逻辑:终止部分或全部死锁进程,释放其持有的资源,解除死锁。根据终止策略可分为两种:

  1. 一次性终止所有死锁进程:直接终止所有参与死锁的进程,快速释放资源,但可能终止核心进程,造成较大损失;
  2. 按优先级逐步终止:优先终止低优先级、非核心的死锁进程,每终止一个就检测一次死锁是否解除,直到死锁消失。这种方式能最大程度减少损失,是更优的选择。

代码示例:

运行

#include <vector>
struct Process { int pid; void terminate() {} void release() {} };
std::vector<Process> get_deadlocked(); // 检测死锁进程

// 一次性终止
void kill_all() {
    auto procs = get_deadlocked();
    for (auto& p : procs) { p.terminate(); p.release(); }
}

// 逐步终止
const int PRIO = 8;
std::vector<Process> sort_by_prio(std::vector<Process> procs);
void kill_grade() {
    auto procs = sort_by_prio(get_deadlocked());
    for (auto& p : procs) {
        if (p.priority < PRIO) {
            p.terminate(); p.release();
            if (get_deadlocked().empty()) break;
        }
    }
}

🔄 3. 剥夺资源

核心逻辑:不终止进程,而是强制剥夺死锁进程持有的资源,分配给等待该资源的进程,打破 “不可剥夺” 条件。根据剥夺方式可分为:

  1. 逐步剥夺:每次仅剥夺死锁进程的一个非核心资源,分配后检测死锁是否解除,未解除则继续剥夺,风险低但效率稍慢;
  2. 一次性剥夺:直接剥夺死锁进程的所有资源,一次性释放并分配,效率高但可能导致被剥夺进程运行异常。

代码示例:

运行

int select_res(const Process& p); // 选择要剥夺的资源
void alloc_to_wait(int res);      // 分配资源给等待进程

// 逐步剥夺
void preempt_grade(Process& p) {
    while (!get_deadlocked().empty()) {
        int res = select_res(p);
        p.force_release(res);
        alloc_to_wait(res);
    }
}

// 一次性剥夺
std::vector<int> get_all_res(const Process& p);
void preempt_all(Process& p) {
    auto res = get_all_res(p);
    p.force_release_all();
    for (int r : res) alloc_to_wait(r);
}

🔄 4. 进程回退

核心逻辑:为进程设置检查点(Checkpoint),定期保存进程的运行状态和资源持有情况。当检测到死锁时,将进程回退到死锁发生前的最近一个安全检查点,释放死锁相关的资源请求,让进程重新执行。这种方式能避免进程终止和数据丢失,适合交易系统、数据库系统等对数据完整性要求高的场景,但缺点是难以处理已产生的外部副作用(如文件修改、网络消息发送)。

代码示例:

cpp

#include <vector>
struct Checkpoint { int id; /* 状态和资源快照 */ };
class RecoverProc {
    std::vector<Checkpoint> cps;
    void save_cp() { cps.push_back({/* 保存快照 */}); } // 保存检查点
    void rollback() { // 回退到最近检查点
        auto cp = cps.back();
        // 恢复状态和资源
        cps.pop_back();
    }
public:
    void run() {
        while (true) {
            save_cp(); // 定期保存检查点
            if (deadlock_detected()) rollback(); // 死锁则回退
            // 业务逻辑
        }
    }
};

关键总结

  1. 死锁发生的前提:必须同时满足互斥、占有并请求、不可剥夺、循环等待四大必要条件;
  2. 解决死锁的优先级:设计阶段预防(如统一锁顺序)>运行时避免>发生后检测>最后恢复;
  3. 核心原则:预防死锁的成本远低于检测和恢复,工程中应优先通过合理的资源分配规则、进程执行逻辑避免死锁,仅在无法预防时才考虑检测和恢复。
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐