Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

背景

一个进程中的多个线程是共享进程空间的数据的,当线程并发执行时,会出现线程安全问题,也就是多个线程对共享数据进行修改时导致数据不一致的问题。为了解决线程并发问题需要对共享资源进行互斥访问,通过互斥锁实现。如果获取锁的顺序不当,很可能会造成死锁。那发生死锁的时候怎么去排查呢?

通过创造产生死锁的4个必要条件模拟死锁的发生

系统产生死锁的四个必要条件

· 互斥

· 请求保持

· 不可剥夺

· 环路等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex mtxA;
mutex mtxB;
mutex mtxC;
void taskA() {
lock_guard<mutex> lockA(mtxA);
cout << "thread A get lockA!" << endl;
this_thread::sleep_for(chrono::seconds(1));
lock_guard<mutex> lockB(mtxB);
cout << "thread A get lockA and lockB!" << endl;
cout << "thread A release lockA and lockB!" << endl;

}
void taskB() {
lock_guard<mutex> lockB(mtxB);
cout << "thread B get lockB!" << endl;
this_thread::sleep_for(chrono::seconds(1));
lock_guard<mutex> lockA(mtxA);
cout << "thread B get lockB and lockA!" << endl;
cout << "thread B release lockB and lockA!" << endl;
}
void taskC() {
lock_guard<mutex> lockC(mtxC);
cout << "thread C get lockC!" << endl;
this_thread::sleep_for(chrono::seconds(1));
lock_guard<mutex> lockA(mtxA);
cout << "thread C get lockC and lockA!" << endl;
cout << "thread C release lockC and lockB!" << endl;
}
int main() {
thread t1(taskA);
thread t2(taskB);
thread t3(taskC);
t1.join();
t2.join();
t3.join();
return 0;
}

代码分析

线程A在执行时,首先会获取lockA,然后等待1s尝试获取 lockB; 而线程B在执行时首先获取 lockB, 等待1s后尝试获取 lockA,这里不会出现线程A既拿到 lockA 又拿到 lockB,因为线程A等待1s的时间内足够让线程B拿到 lockB,此时线程 A 和线程 B形成环路等待,造成死锁。线程C在拿到lockC 后等待1s尝试获取lockA,此时lockA由线程A持有,所以线程C也不会获得lockA,一直阻塞。

代码编译

-g 表示可以用于 gdb 调试

1
g++ -std=c++11 -pthread deadLock.cpp -o deadLock -g

代码执行

1
./deadLock 

执行结果:

线程A、线程B、线程C都出现了死锁

image-20250421212447737

静态查看系统中的所有进程

1
ps aux

静态查看系统中指定进程—使用管道

1
2
ps aux | grep 进程名
ps aux | grep deadLock

image-20250421220534255

deadLock进程的pid 是50357

动态查看系统中所有线程的情况

1
top -H

动态查看查看 pid = ××× 进程中的所有线程

1
top -H -p ×××

image-20250421220643417

通过top命令可以大致定位发生死锁的进程或线程

从图中可以看出 deadLock 进程中存在4个线程, 分别是 threadA、threadB、threadC、main,top命令是动态展示线程在系统中的运行情况,因为这4个线程的CPU和内存的利用率一直都是0, 所以极有可能发生了死锁。

通过gdb命令可以具体排查发生的死锁进程或线程

gdb 的 attach 命令可以对正在运行的进程进行调试,用法如下

1
2
3
4
5
调试 pid 进程: gdb attach <pid>

bt : 查看函数调用栈的信息,查看程序的调用过程

thread id : 切换到具体的线程 id

使用root权限调试threadLock进程(pid=50357)

1
2
su 
gdb attach 50357

打印出所有的线程信息

1
info thread

image-20250421221430137

通过 info thread 打印所有线程的信息,可以看出一共有4个线程,线程pid分别是:50357、50358、50359、50360。

通过thread id切换线程, bt 看线程 栈

1
2
thread 1
bt

image-20250421222546542

可以看出 pid = 50357 是 main 进程

通过thread 2切换到线程2, bt 看线程 栈

1
2
thread 2
bt

image-20250421223132510

可以看出 pid = 50358 是 线程A

通过thread 3切换到线程3, bt 看线程 栈

1
2
thread 3
bt

image-20250421223429438

可以看出 pid = 50359 是 线程B

通过thread 4切换到线程4, bt 看线程 栈

1
2
thread 4
bt

image-20250421223649266

可以看出 pid = 50360 是 线程C

通过 p mtxA 查看 lockA 被谁持有

1
p  mtxA

image-20250421224019783

可以看出 lockA 被 pid = 50358 线程持有, 也就是被线程A持有

通过 p mtxB 查看 lockB被谁持有

1
p  mtxB

image-20250421224246166

可以看出 lockB 被 pid = 50359 线程持有, 也就是被线程B持有

通过 p mtxC 查看 lockC被谁持有

1
p  mtxC

image-20250421224431118

可以看出 lockC 被 pid = 50360 线程持有, 也就是被线程C持有

小节:mtxA 被LWP=50358的线程持有( threadA),mtxB 被LWP=50359的线程持有( threadB),mtxC 被LWP=50360的线程持有( threadC)。接下来我们看看每个threadA、threadB、threadC被阻塞在了什么地方。

image-20250421225328905

我们可以看出,线程A被阻塞在了deadLock.cpp的第12行,定位代码的第 12 行,线程A正在获取lockB

image-20250421225536907

同理线程B被阻塞在了获取 lock A , C线程被阻塞在获取lockA

总结

死锁产生的原因

线程A已经持有lockA, 线程B已经持有lockB,线程A在不释放 lockA 的情况下获取lockB , 线程B在不释放 lockB的情况下获取 lockA, 形成了环路等待,导致了死锁。

如何排查死锁

· 通过 ps aux|grep 进程名获取进程的 pid

· 通过top -H -p pid 查看进程中线程的CPU和内存使用率,如果使用率为0, 那么它可能就是死锁线程

· 最后通过 gdb 调试线程,通过 bt 查看线程的栈的调用情况具体定位死锁原因

解决死锁的方法

  1. 杀死线程B,让线程B释放lockB ,这是简单却不有效的解决方法,只能暂时解决死锁问题

  2. 让所有的线程在获取锁的时候都按照相同的顺序获取锁,顺序获取锁是解决死锁最简单高效、可维护的方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    #include<iostream>
    #include<thread>
    #include<mutex>
    using namespace std;
    mutex mtxA;
    mutex mtxB;
    mutex mtxC;
    void taskA() {
    lock_guard<mutex> lockA(mtxA);
    cout << "thread A get lockA!" << endl;
    this_thread::sleep_for(chrono::seconds(1));
    lock_guard<mutex> lockB(mtxB);
    cout << "thread A get lockA and lockB!" << endl;
    cout << "thread A release lockA and lockB!" << endl;

    }
    void taskB() {
    lock_guard<mutex> lockB(mtxA);
    cout << "thread B get lockB!" << endl;
    this_thread::sleep_for(chrono::seconds(1));
    lock_guard<mutex> lockA(mtxB);
    cout << "thread B get lockB and lockA!" << endl;
    cout << "thread B release lockB and lockA!" << endl;
    }
    void taskC() {
    lock_guard<mutex> lockC(mtxA);
    cout << "thread C get lockC!" << endl;
    this_thread::sleep_for(chrono::seconds(1));
    lock_guard<mutex> lockA(mtxC);
    cout << "thread C get lockC and lockA!" << endl;
    cout << "thread C release lockC and lockB!" << endl;
    }
    int main() {
    thread t1(taskA);
    thread t2(taskB);
    thread t3(taskC);
    t1.join();
    t2.join();
    t3.join();
    system("pause");
    return 0;
    }

    运行结果

    image-20250422155826177

  3. 给锁加上一个时间机制,超过设定时间时,线程释放自己占用的锁

评论