死锁的概念/产生/规避/调试
1.死锁概念
死锁是指多个并发进程或线程在运行过程中,由于竞争资源而造成一种互相等待的现象。此时每个线程都在等待其他线程释放资源,而没有任何一方能继续执行,从而导致系统整体无法推进。
2.产生死锁的四个必要条件
- 互斥条件,共享资源 X 和 Y 只能被一个线程占用
- 持有并等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不 释放共享资源 X
- 不可剥夺,其他线程不能强行抢占线程 T1 占有的资源
- 循环等待,线程 T1 等待线程 T2 占有的资源 ,线程 T2 等待线程 T1 占有 的资源 ,就是循环等待
3.死锁的常见场景
示例1:同一线程中未解锁的情况下,重复加锁导致死锁,类似于嵌套锁
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx;
int main() {
cout << "start ..." << endl;
mtx.lock();
cout << "first lock ..." << endl;
mtx.lock();
cout << "second lock ..." << endl;
cout << "endl ..." << endl;
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ g++ test6.cpp -o test6 -pthread;./test6
start ...
first lock ...
解决死锁问题,可以将Line6修改成recursive_mutex mtx,输出:
chenglin@ubuntu:~/work/Thread$ g++ test11.cpp -o test11 -pthread;./test11
start ...
first lock ...
second lock ...
endl ...
示例:不同线程中多个锁锁定顺序不一致导致死锁
#include <thread>
#include <iostream>
#include <mutex>
using namespace std;
mutex mtx1;
mutex mtx2;
void testDiedLock1()
{
mtx1.lock();
this_thread::sleep_for(100ms);
cout << "testDiedLock1 mtx1 lock" << endl;
mtx2.lock();
this_thread::sleep_for(100ms);
cout << "testDiedLock1 mtx2 lock" << endl;
mtx2.unlock();
cout << "testDiedLock1 mtx2 unlock" << endl;
mtx1.unlock();
cout << "testDiedLock1 mtx1 unlock" << endl;
}
void testDiedLock2()
{
mtx2.lock();
this_thread::sleep_for(100ms);
cout << "testDiedLock2 mtx2 lock" << endl;
mtx1.lock();
this_thread::sleep_for(100ms);
cout << "testDiedLock2 mtx1 lock" << endl;
mtx1.unlock();
cout << "testDiedLock2 mtx1 unlock" << endl;
mtx2.unlock();
cout << "testDiedLock2 mtx2 unlock" << endl;
}
int main() {
thread t1(testDiedLock1);
thread t2(testDiedLock2);
t1.join();
t2.join();
return 0;
}
输出:
chenglin@ubuntu:~/work/Thread$ ./test10
testDiedLock2 mtx2 lock
testDiedLock1 mtx1 lock
解决死锁问题,可以将Line41修改成thread t2(testDiedLock1),保证锁定顺序一致,输出:
chenglin@ubuntu:~/work/Thread$ g++ test10.cpp -o test10 -pthread;./test10
testDiedLock1 mtx1 lock
testDiedLock1 mtx2 lock
testDiedLock1 mtx2 unlock
testDiedLock1 mtx1 unlock
testDiedLock1 mtx1 lock
testDiedLock1 mtx2 lock
testDiedLock1 mtx2 unlock
testDiedLock1 mtx1 unlock
4.规避死锁的方法
- 破坏互斥条件
- 使用无锁编程
- 原子操作
- 无锁队列
- 读写锁中的读锁
- 使用std::shared_lock
- 使用无锁编程
- 破坏持有并等待
- 使用try_lock
- 避免嵌套锁,使用递归锁std:recursive_mutex
- 使用std::lock同时获取多把锁
- 使用std::scoped_lock同时获取多把锁
- 引入锁的等级制度
- 破坏不可剥夺
- 使用lock_for超时机制
- 破坏循环等待
- 使用相同顺序获取锁
5.死锁问题调试
5.1 windows平台
- 使用调试器的线程和并行堆栈窗口
- 设置断点并启动调试:在 Visual Studio 中打开项目,设置好断点后,按
F5
启动调试。 - 查看线程窗口:在 “调试” 菜单中选择 “窗口”->“线程”,打开线程窗口。在程序运行到可能出现死锁的位置后,点击 “全部中断” 按钮,此时线程窗口会显示当前所有线程的状态,你可以查看每个线程的执行情况,检查是否有线程卡在某个特定的锁上。
- 查看并行堆栈窗口:通过 “调试” 菜单中的 “窗口”->“并行堆栈” 打开该窗口。它会以图形化的方式展示线程之间的调用栈和锁的持有情况,能帮助你更直观地识别可能的死锁。例如,当两个或多个线程相互等待对方释放锁时,在并行堆栈窗口中可以清晰地看到这种循环等待的关系。
- 设置断点并启动调试:在 Visual Studio 中打开项目,设置好断点后,按
- 分析 Dump 文件
- 生成 Dump 文件:可以使用 Visual Studio 自带的功能或命令行工具(如 dotnet-dump)来收集转储文件。例如在程序运行出现疑似死锁的情况时,使用工具收集此时的内存等信息生成 Dump 文件。
- 在 Visual Studio 中分析 Dump 文件:将生成的 Dump 文件拖入 Visual Studio,然后点击运行诊断分析。Visual Studio 会尝试分析其中的信息,检测是否存在死锁,并给出相关的分析结果,如显示已死锁的线程等。还可以结合调试模式下的各种监视窗口,如并行堆栈窗口等,进一步分析死锁的具体情况,定位到发生死锁的代码位置。
- 使用性能分析工具:Visual Studio 的性能分析工具也可以帮助检测死锁。例如,通过性能探查器可以分析多个线程对共享资源的竞争情况,检测可能的死锁条件。在性能分析会话中,它可能会显示线程的等待时间、锁的获取和释放时间等信息,通过分析这些数据,可以发现潜在的死锁问题。
5.2 linux平台
1.使用 Valgrind 的 Helgrind 工具
Helgrind 是 Valgrind 的一个工具,专门用于检测多线程程序中的竞态条件和死锁。
安装 Valgrind:
对于基于 Debian 或 Ubuntu 的系统:
sudo apt - get update
sudo apt - get install valgrind
对于基于 Red Hat 或 CentOS 的系统:
sudo yum install valgrind
使用 Helgrind 检测死锁:
编译你的 C++ 程序时,确保添加了调试信息(-g
选项)。例如:
g++ -g -pthread your_program.cpp -o your_program
然后使用 Helgrind 运行程序:
valgrind --tool = helgrind ./your_program
Helgrind 会输出详细的报告,指出可能的死锁情况,包括涉及的线程和锁的获取顺序。
2. 使用 GDB 调试器
虽然 GDB 本身不能直接检测死锁,但结合一些命令和脚本,可以帮助我们发现死锁。
使用 GDB 附加到运行中的进程:
假设你的程序已经在运行,获取其进程 ID(PID),然后使用 GDB 附加到该进程:
gdb -p <PID>
查看线程状态:
在 GDB 中,使用 info threads
命令查看所有线程的状态。这可以帮助你发现线程是否停滞在某个操作上,例如等待锁。
(gdb) info threads
查看线程堆栈:
使用 thread <thread - id>
切换到特定线程,然后使用 bt
查看其堆栈跟踪,以了解线程正在执行的操作。
(gdb) thread 1
(gdb) bt
编写 GDB 脚本自动化检测:
可以编写 GDB 脚本定期检查线程状态和堆栈,以便更方便地发现死锁。例如,以下是一个简单的 GDB 脚本示例(deadlock - check.gdb
):
define deadlock - check
info threads
set $i = 1
while ($i <= $threadnum)
thread $i
bt
set $i = $i + 1
end
end
在 GDB 中加载该脚本并定期执行:
source deadlock - check.gdb
while 1
deadlock - check
sleep 5
end
3. 应用程序级别的死锁检测算法
在应用程序代码中实现死锁检测算法,例如资源分配图算法。
资源分配图算法原理:
构建资源分配图,其中节点表示进程和资源,边表示资源的请求或分配关系。通过对这个图进行分析,检查是否存在环。如果存在环,并且环中的每个资源都被占用,那么就可能存在死锁。
示例代码(简化的资源分配图检测概念代码):
#include <iostream>
#include <vector>
#include <stack>
// 假设资源类型和进程类型都是整数
using Resource = int;
using Process = int;
// 资源分配图的数据结构
using Graph = std::vector<std::vector<bool>>;
// 检测是否存在环
bool hasCycle(const Graph& graph, int start, std::vector<bool>& visited, std::vector<bool>& recursionStack) {
visited[start] = true;
recursionStack[start] = true;
for (size_t i = 0; i < graph.size(); ++i) {
if (graph[start][i]) {
if (!visited[i] && hasCycle(graph, i, visited, recursionStack)) {
return true;
} else if (recursionStack[i]) {
return true;
}
}
}
recursionStack[start] = false;
return false;
}
// 检测死锁
bool detectDeadlock(const Graph& graph) {
int numNodes = graph.size();
std::vector<bool> visited(numNodes, false);
std::vector<bool> recursionStack(numNodes, false);
for (int i = 0; i < numNodes; ++i) {
if (!visited[i] && hasCycle(graph, i, visited, recursionStack)) {
return true;
}
}
return false;
}
通过上述方法,在 Linux 环境下可以有效地检测死锁。每种方法都有其优缺点,应根据具体情况选择合适的方法。