死锁产生的四个必要条件
- 互斥条件:资源一次只能被一个线程使用,即某个资源在一段时间内只能由一个线程占有。例如,一个文件在同一时刻只能被一个线程打开进行写操作。
- 占有并等待条件:一个线程已经持有了至少一个资源,但又请求新的资源,并且在等待新资源的同时,不会释放已持有的资源。比如线程 A 持有资源 R1,又请求资源 R2,在等待 R2 时不释放 R1。
- 不可剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺,只能由该线程自己释放。例如线程获取的锁,其他线程不能强行抢走。
- 循环等待条件:存在一个线程 - 资源的循环链,链中每个线程都在等待下一个线程所持有的资源。例如线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,线程 C 又等待线程 A 持有的资源。
避免死锁的策略及代码实现
- 破坏占有并等待条件
- 策略:在申请新的资源前,先释放已持有的所有资源,然后一次性申请所需的全部资源。
- 代码实现:
#include <mutex>
#include <thread>
#include <iostream>
std::mutex m1, m2;
void threadFunction() {
// 先释放所有已持有的锁
std::unique_lock<std::mutex> lock1(m1, std::defer_lock);
std::unique_lock<std::mutex> lock2(m2, std::defer_lock);
// 一次性申请所有需要的锁
std::lock(lock1, lock2);
std::cout << "Thread is working with both resources" << std::endl;
// 工作完成后释放锁
lock1.unlock();
lock2.unlock();
}
int main() {
std::thread t(threadFunction);
t.join();
return 0;
}
- 破坏循环等待条件
- 策略:对资源进行排序,线程按照固定的顺序获取资源。
- 代码实现:
#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <algorithm>
std::mutex m1, m2;
void threadFunction() {
std::vector<std::mutex*> resources = {&m1, &m2};
std::sort(resources.begin(), resources.end(), [](std::mutex* a, std::mutex* b) {
return a < b;
});
for (auto mutexPtr : resources) {
std::unique_lock<std::mutex> lock(*mutexPtr);
}
std::cout << "Thread is working with both resources" << std::endl;
// 锁会在离开作用域时自动释放
}
int main() {
std::thread t(threadFunction);
t.join();
return 0;
}
定位和解决死锁问题的步骤
- 收集信息
- 日志记录:检查程序运行过程中的日志,查看是否有异常情况记录,例如线程进入等待状态但未按预期执行下一步操作等信息。
- 系统监控:使用系统工具(如 Linux 下的
top
、ps
等命令)查看进程状态,确定程序是否处于死锁状态,以及死锁进程占用的资源情况。
- 使用调试工具
- GDB(GNU Debugger):如果程序运行在支持 GDB 的环境中,使用 GDB 附加到死锁进程。通过
info threads
命令查看所有线程的状态,找到处于阻塞状态的线程。然后使用 thread <thread_id>
切换到具体线程,通过 bt
(backtrace)命令查看线程的调用栈,分析线程在等待什么资源。
- Valgrind:Valgrind 工具集的
helgrind
工具可以检测多线程程序中的竞争条件和死锁。运行程序时使用 helgrind
,它会报告可能导致死锁的资源竞争点。
- 分析代码
- 检查资源获取顺序:查看代码中资源获取的顺序,确认是否存在循环等待的可能。如果发现资源获取顺序混乱,按照破坏循环等待条件的策略进行修改。
- 检查锁的使用:检查锁的获取和释放逻辑,确保没有线程在持有锁的情况下无限期等待其他资源,遵循破坏占有并等待条件的原则优化代码。
- 修复和验证
- 修复代码:根据分析结果对代码进行修改,如调整资源获取顺序、优化锁的使用逻辑等。
- 重新测试:修改代码后,进行全面的测试,包括多线程并发测试,确保死锁问题得到解决,并且没有引入新的问题。