C++ 死锁检测基础思路详解
一、理论部分
死锁(Deadlock)是并发编程中最棘手的问题之一。不同于内存泄漏可以通过工具最终定位,死锁一旦发生,往往导致系统彻底卡死,且难以复现。
死锁的现象举一个简单的例子,如下图所示,3个线程都在运行,且图中资源均一次只能被一个线程占用,线程A占用资源1,线程B占用资源2,线程C占用资源3,此时线程A不释放资源1且想去占用资源2,而线程B也不释放资源2并且想去占用资源3,而线程C同样不释放资源3去占用资源1。这样,线程A, B, C 都因为获取不到足够的资源而一直陷入等待状态。这种现象就是死锁。

死锁产生的四个必要条件,也就是死锁产生的原因如下,缺一不可:
| 条件 | 说明 |
| 互斥条件 | 资源一次只能被一个线程占用 |
| 持有且等待 | 线程持有资源同时请求新资源 |
| 不可抢占 | 资源不能被强制释放 |
| 循环等待 | 形成线程-资源的循环链 |
这四个必要条件,只要打破一个,就不会形成死锁,但一般来说我们不会去打破第一个互斥条件,因为这一般是资源自带的性质,我们无法避免。比如说买票时的车票数,不同人看到的剩余票数应该是一致的,这无法避免。
而要打破死锁,首先需要的是检测到死锁。那么如何检测呢?回到刚才的图我们可以发现,形成死锁后图中出现了环,也就是说我们可以将线程与资源占用关系抽象成图之后,检测图中是否形成环回路,只要有环,那就出现了死锁,进而采取下一步操作。
二、实现部分
我们在此仅实现一个简易化的版本,由于理解死锁检测。
1. 数据结构设计
核心数据结构
struct source_type {
uint64 id; // 线程ID或锁地址
enum Type type; // 类型:PROCESS 或 RESOURCE(虽然代码中只用到了PROCESS)
uint64 lock_id; // 锁ID(用于locklist)
int degress; // 锁的等待计数
};
struct vertex {
struct source_type s; // 顶点数据
struct vertex *next; // 邻接表指针
};任务图(等待图)
struct task_graph {
struct vertex list[MAX]; // 顶点数组(邻接表头)
int num; // 顶点数量
struct source_type locklist[MAX]; // 锁持有表
int lockidx; // 锁数量
pthread_mutex_t mutex; // 保护图结构的锁(实际未使用)
};- 邻接表:list[MAX] 存储所有线程顶点,next 指向该线程等待的其他线程
- 锁持有表:记录每个锁当前被哪个线程持有
2. 核心逻辑
核心规则
代码的逻辑是当线程1想要持有锁时,先查询锁持有表,如果锁没有被占用,那就直接使用锁并在锁持有表中新增一条对应的记录。如果锁被占用了,就在图中连一条指向占有线程2的边。
当线程T1试图获取已被T2持有的锁L时:
添加边:T1 → T2
表示T1在等待T2释放锁三个关键函数(部分伪代码)
// 1. 加锁前:如果锁已被其他线程持有,建立等待关系
void lock_before(tid, lockaddr) {
if (锁已被其他线程T2持有) {
添加边:当前线程T1 → T2
lock.degress++ // 等待计数增加
}
}
// 2. 加锁后:更新锁的持有者
void lock_after(tid, lockaddr) {
if (锁是空闲的) {
记录当前线程持有该锁
} else {
移除之前建立的等待边(因为已经获得锁)
更新锁的持有者为当前线程
}
}
// 3. 解锁后:如果没人等待,清空锁记录
void unlock_after(tid, lockaddr) {
if (锁的degress == 0) {
清空锁的持有信息
}
}3. 死锁检测算法
在以上接口的基础上,我们再添加一个检测图中环的算法就能实现死锁检测。最暴力的做法是使用DFS 但不推荐。推荐使用 Tarjan 算法来检测环,一个环一定是一个有向图的一个强连通分量,通过这个性质来实现死锁检测。
4. 钩子机制(Hooking)
最后是通过钩子机制获取并修改原始函数指针,将pthread_mutex_lock和pthread_mutex_unlock改写逻辑:
// hook
// define
typedef int (*pthread_mutex_lock_t)(pthread_mutex_t *mutex);
pthread_mutex_lock_t pthread_mutex_lock_f = NULL;
typedef int (*pthread_mutex_unlock_t)(pthread_mutex_t *mutex);
pthread_mutex_unlock_t pthread_mutex_unlock_f = NULL;
// implement
int pthread_mutex_lock(pthread_mutex_t *mutex) {
pthread_t selfid = pthread_self();
lock_before((uint64_t)selfid, (uint64_t)mutex);
pthread_mutex_lock_f(mutex);
lock_after((uint64_t)selfid, (uint64_t)mutex);
}
int pthread_mutex_unlock(pthread_mutex_t *mutex) {
pthread_mutex_unlock_f(mutex);
pthread_t selfid = pthread_self();
unlock_after((uint64_t)selfid, (uint64_t)mutex);
}
// init
void init_hook(void) {
if (!pthread_mutex_lock_f)
pthread_mutex_lock_f = dlsym(RTLD_NEXT, "pthread_mutex_lock");
if (!pthread_mutex_unlock_f)
pthread_mutex_unlock_f = dlsym(RTLD_NEXT, "pthread_mutex_unlock");
}以上是死锁检测的一个基本思路,将线程与资源及其关系抽象成有向图,对图进行环回路检测。而在使用死锁检测时,可以用一个独立的线程监控,不影响主程序性能。
到此这篇关于C++ 死锁检测基础思路的文章就介绍到这了,更多相关C++ 死锁检测内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
C++基于CreateToolhelp32Snapshot获取系统进程实例
这篇文章主要介绍了C++基于CreateToolhelp32Snapshot获取系统进程实例,是Windows应用程序设计中非常实用的技巧,需要的朋友可以参考下2014-10-10


最新评论