Linux 死锁
什么是死锁?
死锁是指一组并发进程(或线程)彼此等待对方所持有的资源,从而导致所有进程都无法继续向前执行的一种僵持状态。
可以把它想象成一个交通堵塞:
-
四辆车在一个十字路口相遇。
-
每辆车都需要其他车让出道路才能通过。
-
但没有任何一辆车愿意后退。
-
结果就是所有车都卡在原地,谁也动不了。
在计算机中,这些“道路”就是互斥资源,如:锁、信号量、文件句柄、套接字等。
死锁产生的四个必要条件
这四个条件必须同时满足,死锁才会发生。只要破坏其中任意一个,就能预防死锁。
-
互斥条件:一个资源每次只能被一个进程使用。
- 例如:一把锁一次只能被一个线程持有。
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 例如:线程 A 拿着锁 L1 不放,同时去请求锁 L2。
-
不剥夺条件:进程已获得的资源,在未使用完之前,不能被强行剥夺,只能由该进程主动释放。
- 例如:操作系统不能强行把线程 A 手中的锁 L1 抢过来给线程 B。
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
- 例如:线程 A 等待线程 B 持有的资源,线程 B 又在等待线程 A 持有的资源。
Linux C++ 代码示例
下面是一个经典的、必然会发生死锁的双线程示例:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1, mutex2;
void threadA() {
std::unique_lock<std::mutex> lock1(mutex1); // 1. 线程A锁住mutex1
std::cout << "Thread A acquired mutex1" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 增加不确定性,确保线程B能锁住mutex2
std::unique_lock<std::mutex> lock2(mutex2); // 3. 线程A尝试获取mutex2(但此时已被线程B锁住) -> 阻塞!
std::cout << "Thread A acquired mutex2" << std::endl; // 永远不会执行
}
void threadB() {
std::unique_lock<std::mutex> lock2(mutex2); // 2. 线程B锁住mutex2
std::cout << "Thread B acquired mutex2" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::unique_lock<std::mutex> lock1(mutex1); // 4. 线程B尝试获取mutex1(但此时已被线程A锁住) -> 阻塞!
std::cout << "Thread B acquired mutex1" << std::endl; // 永远不会执行
}
int main() {
std::thread t1(threadA);
std::thread t2(threadB);
t1.join();
t2.join();
std::cout << "Main thread exits" << std::endl;
return 0;
}
分析:
-
程序启动两个线程 t1 和 t2。
-
t1 先锁住 mutex1,t2 先锁住 mutex2。
-
接着,t1 尝试去锁 mutex2(但它在 t2 手里),t2 尝试去锁 mutex1(但它在 t1 手里)。
-
双方都持有一把锁,并无限期地等待对方释放另一把锁。循环等待形成,程序卡死,无法输出最后一条信息。
这就是一个典型的死锁。
在 Linux 中如何检测和调试死锁?
1.观察现象:
程序表现为“卡住”,不产生任何输出,CPU 占用率可能很低(因为线程在阻塞等待,而不是忙循环)。
2.使用调试工具:
Linux 提供了强大的命令行工具来帮助诊断。
-
gdb (GNU Debugger)
-
首先,用 ps -aux | grep <program_name> 找到卡死程序的 PID。
-
然后,用 gdb 附加到该进程:sudo gdb -p
-
在 gdb 中,输入 thread apply all bt(查看所有线程的调用栈回溯)。
-
分析结果:你会看到两个或多个线程的栈回溯,它们都卡在 __lll_lock_wait 或类似的函数中,这正是等待锁的地方。通过查看栈帧,你可以清楚地看到每个线程在哪个源代码行正在请求哪把锁。
-
-
pstack
-
这是一个脚本,可以快速打印一个进程的栈跟踪:pstack
-
其输出类似于 thread apply all bt,但更快捷。
-
-
strace / ltrace
-
strace 跟踪系统调用。
-
ltrace 跟踪库函数调用。
-
你可以用它们启动程序(strace ./my_program)或附加到已运行进程(strace -p ),观察程序卡在哪个系统调用(很可能是 futex,这是 Linux 中实现锁的常见系统调用)。
-
如何避免和预防死锁?
记住死锁的四个必要条件,破坏其中之一即可。
-
破坏“请求与保持”条件:一次性申请所有需要的资源。
- 缺点:可能降低资源利用率,因为有些资源可能很晚才用到。
-
破坏“不剥夺”条件:如果一个进程请求资源失败,则释放它当前持有的所有资源。
- 缺点:实现复杂,可能导致任务重复执行。
-
破坏“循环等待”条件:这是最常用且有效的方法。
-
对资源进行排序:给所有锁定义一个全局的获取顺序。每个线程都必须按照这个顺序来申请锁。
-
修改上面的例子:让 threadA 和 threadB 都按照先 mutex1 后 mutex2 的顺序申请锁。这样,threadA 拿到 mutex1 后,threadB 在申请 mutex1 时就会阻塞,直到 threadA 释放 mutex1 和 mutex2。这样就不会形成循环。
-
void threadA() {
std::unique_lock<std::mutex> lock1(mutex1);
std::unique_lock<std::mutex> lock2(mutex2); // 顺序和threadB一致
// ... 安全地操作资源
}
void threadB() {
std::unique_lock<std::mutex> lock1(mutex1); // 先尝试获取mutex1,而不是mutex2
std::unique_lock<std::mutex> lock2(mutex2);
// ... 安全地操作资源
}
使用更高级的抽象:
- std::lock():C++ 标准库提供的函数,可以一次性锁住多个互斥量,并且避免了死锁(它内部使用特殊的算法来避免循环等待)。这是首选方案
#include <mutex>
...
void safe_thread() {
std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
std::lock(lock1, lock2); // 一次性锁住两个,不会死锁
// ... 临界区操作
}
避免嵌套锁
- 尽量减少持有锁的同时去获取另一把锁。如果逻辑不可避免,务必使用上述的“锁排序”或 std::lock。
小结
| 方面 | 要点 |
|---|---|
| 定义 | 一组进程/线程相互等待,导致所有进程都无法执行。 |
| 必要条件 | 互斥、请求与保持、不剥夺、循环等待。 |
| 调试方法 | 使用 gdb -p + thread apply all bt,或 pstack 。 |
| 预防关键 | 破坏循环等待:对锁进行排序或使用 std::lock() 一次性获取多个锁。 |
| 最佳实践 | 锁的粒度要细,持锁时间要短,尽量避免嵌套锁,必须嵌套时规定顺序。 |
更多推荐
所有评论(0)