死锁的概念/产生/规避/调试

死锁的概念/产生/规避/调试

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启动调试。
    • 查看线程窗口:在 “调试” 菜单中选择 “窗口”->“线程”,打开线程窗口。在程序运行到可能出现死锁的位置后,点击 “全部中断” 按钮,此时线程窗口会显示当前所有线程的状态,你可以查看每个线程的执行情况,检查是否有线程卡在某个特定的锁上。
    • 查看并行堆栈窗口:通过 “调试” 菜单中的 “窗口”->“并行堆栈” 打开该窗口。它会以图形化的方式展示线程之间的调用栈和锁的持有情况,能帮助你更直观地识别可能的死锁。例如,当两个或多个线程相互等待对方释放锁时,在并行堆栈窗口中可以清晰地看到这种循环等待的关系。
  • 分析 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 环境下可以有效地检测死锁。每种方法都有其优缺点,应根据具体情况选择合适的方法。

参考链接

从腾讯面试题入手,带你吃透C++死锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值