Linux下排除死锁详细教程(基于C++11、GDB)

Linux下排除死锁详细教程(基于C++11、GDB)

1. 前言

在实际编写项目的过程中,经常涉及到多线程。多线程的程序编程很大概率会涉及到线程安全的问题,因此往往使用互斥锁来保证线程安全。

然而,互斥锁的使用却经常会导致另外一个问题:死锁。

所谓死锁,通俗的讲就是有两个共享资源,一个在你手上,一个在我手上。我等着你用完把另一个给我,你等我用完给你,这样相互等待就形成了死锁。

所以,在这里,基于Linux的环境,使用C++11提供的多线程编程来模拟死锁,并尝试从“不知情”的角度,使用shell + gbd 进行死锁的排查。

2. 模拟死锁

  1 #include <iostream>
  2 #include <thread>
  3 #include <mutex>
  4 
  5 using namespace std;
  6 
  7 mutex mtxA;
  8 mutex mtxB;
  9 
 10 void taskA() {
 11     lock_guard<mutex> lockA(mtxA);
 12     cout << "Thread A get Lock A!" << endl;
 13     this_thread::sleep_for(chrono::seconds(1));
 14     lock_guard<mutex> lockB(mtxB);
 15     cout << "Thread A get Lock A and B!" << endl;
 16     cout << "Thread A relese all source!" << endl;
 17 
 18 }
 19 
 20 void taskB() {
 21     lock_guard<mutex> lockB(mtxB);
 22     cout << "Thread B get Lock B!" << endl;
 23     this_thread::sleep_for(chrono::seconds(1));
 24     lock_guard<mutex> lockA(mtxA);
 25     cout << "Thread B get Lock B and A!" << endl;
 26     cout << "Thread B relese all source!" << endl;
 27 }
 28 
 29 int main() {
 30 
 31     thread t1(taskA);
 32     thread t2(taskB);
 33 
 34     t1.join();
 35     t2.join();
 36 
 37     return 0;
 38 }

这里我们使用了C++11中的多线程编程来模拟了一个死锁的场景。

首先,对于线程t1,会执行工作函数taskA。在taskA里面,线程t1首先会拿到 mtxA 这把互斥锁。然后睡眠1秒,让 线程t2执行工作函数taskB,由线程t2拿到 mtxB 这把互斥锁。这时,线程t1醒过来想拿锁B,而B却在t2手里。t2在拿到 mtxB 时,马上想获取 mtxA,但是在睡眠的t1手里。

因此,线程t1和t2循环等待对方的资源,又释放自己手里的资源,这样就形成了死锁。

我们将这个文件命名为:deadLock.cc ,并使用g++进行编译,生成可执行文件 (tip:注意一定要加上 -pthread,因为C++11里面的多线程库在Linux下也是基于pthread这个库实现的)::

g++ deadLock.cc -pthread -o deadLock -g

如此生成了可执行文件,因此可以运行:

./deadLock

运行结果:

在这里插入图片描述

可以看见,线程t1和t2各自获取了一把锁,同时它们都在等待对面释放手里的锁,因此造成了死锁。

3. 排查死锁

首先,我们怀疑一个程序发生了死锁,首先可以查看该进程CPU利用率、内存利用率的情况。因为如果发生了死锁(这里假设是互斥锁),进程里面发生死锁的线程会处于阻塞的状态,此时基本不占有CPU,因此CPU的利用率、内存占有率将会比较低。 我们可以使用 ps aux 命令来拿到一个进程的状态:

ps aux | grep deadLock

在这里插入图片描述

这里可以看见有两个和deadLock相关的进程:6586、6674。后面一个是执行了grep这个命令之后产生的进程,所以第一个进程是我们想排查的进程。

然后,使用 top 命令来查看进程的CPU利用率、内存占有率:

top -Hp 6586

在这里插入图片描述
可以看见,这个进程里面一共存在三个线程。仔细思考,应该是对应线程t1、t2、和 main线程。它们的CPU利用率、内存都是0,很有可能发生了死锁。

此时,进程已经运行起来了。在实际的项目中,我们一般也不可能把一个进程停掉用GDB调试。因此,只能用GDB 的 attach 命令来跟踪这个进程:

首先需要超级权限,输入密码:

su

然后:

gdb attach 6586

在这里插入图片描述

再查看三个线程的堆栈的调用情况:

thread apply all bt

在这里插入图片描述
(按 enter键)
在这里插入图片描述
(按 enter键)
在这里插入图片描述
可以看见,上面的三张图显示了三个线程的堆栈的调用情况。我们仔细观察每个线程:

在这里插入图片描述
这里红色框中有main,说明线程1是主线程。

在这里插入图片描述
这里红色框中有taskA,说明线程2是线程t1。

在这里插入图片描述
这里红色框中有taskB,说明线程3是线程t2。

因此,到此,我们知道了线程1是main线程、线程2是t1、线程3是t2。所以,下一步我们单独查看每个线程的堆栈调用情况。 使用:

info threads

得到:
在这里插入图片描述
各个线程的索引。

使用 thread + 线程索来切换到某个线程:

thread 1

在这里插入图片描述
使用 bt 来查看堆栈当前线程的堆栈调用:

在这里插入图片描述
可以大概浏览一下,当前线程没有和锁相关的调用,所以大概率死锁不发生在这个线程中。

接下来切换到线程2,再使用bt 查看堆栈:

在这里插入图片描述
在这里插入图片描述

在上面这张图中,从上往下看,找到进程名+行数的组合最后出现的地方,出现在程序的14行。我们用vim看一下程序的14行是什么:

在这里插入图片描述
刚好是线程t1醒来后想去拿锁B的行数。所以基本可以判断,这个线程拿不到锁,一直阻塞等待。

我们再查看线程3的情况 :

在这里插入图片描述
在这里插入图片描述
可以看见,线程3在执行完进程的23行基本阻塞住了,我们再去看看23行是什么:

在这里插入图片描述
可以看见,线程t2想拿锁A,但锁A在t1手里,所以它们俩循环等待对方先释放锁,造成了死锁。

至此,排除基本结束。接下来解除死锁的办法有很多,比如使用资源有序分配法,对于线程t1、t2按相同的顺序分配锁(这里线程t1先A再B,线程t2先B再A,因此死锁了),或者先把锁分配给一个线程,用完再释放全部的锁,这样也能解决死锁的问题。

  • 10
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值