死锁:概念、检测与恢复
一、死锁四大必要条件(缺一不可)
死锁的发生必须同时满足以下四个核心条件,只要打破其中任意一个,就能避免死锁产生。
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();
}
二、死锁核心成因
死锁本质是 “资源分配” 与 “进程执行” 失衡导致的结果,核心成因可分为三类:
- 系统资源不足:CPU、内存、锁、I/O 设备等稀缺资源的总量无法满足所有并发进程的需求,进程为争夺有限资源必然产生竞争,这是死锁发生的根本原因。
- 进程推进顺序不当:进程请求和释放资源的时序不合理,比如部分进程先占资源再请求,部分进程无序请求资源,最终触发持有并请求、循环等待等死锁条件。
- 资源分配策略缺陷:静态分配(进程启动前分配所有所需资源)会导致资源利用率极低,且易造成资源闲置;动态分配(进程运行中按需分配)若时机或规则错误,会加剧资源竞争,增加死锁概率。
三、死锁检测方法
死锁检测的核心目标是及时发现系统中的死锁状态,为后续恢复提供依据,常用方法有两种:
🔍 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. 终止进程
核心逻辑:终止部分或全部死锁进程,释放其持有的资源,解除死锁。根据终止策略可分为两种:
- 一次性终止所有死锁进程:直接终止所有参与死锁的进程,快速释放资源,但可能终止核心进程,造成较大损失;
- 按优先级逐步终止:优先终止低优先级、非核心的死锁进程,每终止一个就检测一次死锁是否解除,直到死锁消失。这种方式能最大程度减少损失,是更优的选择。
代码示例:
运行
#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. 剥夺资源
核心逻辑:不终止进程,而是强制剥夺死锁进程持有的资源,分配给等待该资源的进程,打破 “不可剥夺” 条件。根据剥夺方式可分为:
- 逐步剥夺:每次仅剥夺死锁进程的一个非核心资源,分配后检测死锁是否解除,未解除则继续剥夺,风险低但效率稍慢;
- 一次性剥夺:直接剥夺死锁进程的所有资源,一次性释放并分配,效率高但可能导致被剥夺进程运行异常。
代码示例:
运行
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(); // 死锁则回退
// 业务逻辑
}
}
};
关键总结
- 死锁发生的前提:必须同时满足互斥、占有并请求、不可剥夺、循环等待四大必要条件;
- 解决死锁的优先级:设计阶段预防(如统一锁顺序)>运行时避免>发生后检测>最后恢复;
- 核心原则:预防死锁的成本远低于检测和恢复,工程中应优先通过合理的资源分配规则、进程执行逻辑避免死锁,仅在无法预防时才考虑检测和恢复。
更多推荐
所有评论(0)