背景
一个进程中的多个线程是共享进程空间的数据的,当线程并发执行时,会出现线程安全问题,也就是多个线程对共享数据进行修改时导致数据不一致的问题。为了解决线程并发问题需要对共享资源进行互斥访问,通过互斥锁实现。如果获取锁的顺序不当,很可能会造成死锁。那发生死锁的时候怎么去排查呢?
通过创造产生死锁的4个必要条件模拟死锁的发生
系统产生死锁的四个必要条件
· 互斥
· 请求保持
· 不可剥夺
· 环路等待
1 | #include<iostream> |
代码分析
线程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都出现了死锁

静态查看系统中的所有进程
1 | ps aux |
静态查看系统中指定进程—使用管道
1 | ps aux | grep 进程名 |

deadLock进程的pid 是50357
动态查看系统中所有线程的情况
1 | top -H |
动态查看查看 pid = ××× 进程中的所有线程
1 | top -H -p ××× |

通过top命令可以大致定位发生死锁的进程或线程
从图中可以看出 deadLock 进程中存在4个线程, 分别是 threadA、threadB、threadC、main,top命令是动态展示线程在系统中的运行情况,因为这4个线程的CPU和内存的利用率一直都是0, 所以极有可能发生了死锁。
通过gdb命令可以具体排查发生的死锁进程或线程
gdb 的 attach 命令可以对正在运行的进程进行调试,用法如下
1 | 调试 pid 进程: gdb attach <pid> |
使用root权限调试threadLock进程(pid=50357)
1 | su |
打印出所有的线程信息
1 | info thread |

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

可以看出 pid = 50357 是 main 进程
通过thread 2切换到线程2, bt 看线程 栈
1 | thread 2 |

可以看出 pid = 50358 是 线程A
通过thread 3切换到线程3, bt 看线程 栈
1 | thread 3 |

可以看出 pid = 50359 是 线程B
通过thread 4切换到线程4, bt 看线程 栈
1 | thread 4 |

可以看出 pid = 50360 是 线程C
通过 p mtxA 查看 lockA 被谁持有
1 | p mtxA |

可以看出 lockA 被 pid = 50358 线程持有, 也就是被线程A持有
通过 p mtxB 查看 lockB被谁持有
1 | p mtxB |

可以看出 lockB 被 pid = 50359 线程持有, 也就是被线程B持有
通过 p mtxC 查看 lockC被谁持有
1 | p mtxC |

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

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

同理线程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 查看线程的栈的调用情况具体定位死锁原因
解决死锁的方法
杀死线程B,让线程B释放lockB ,这是简单却不有效的解决方法,只能暂时解决死锁问题
让所有的线程在获取锁的时候都按照相同的顺序获取锁,顺序获取锁是解决死锁最简单高效、可维护的方式
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;
}运行结果

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