做后台开发这些年死锁一直是我觉得最难查的一类问题。它不像崩溃有core文件也不像性能问题能看到火焰图往往就是线上某个服务突然卡住线程堆栈全停在锁等待上业务日志一片空白你只能对着gdb输出一遍遍猜。所以我一直想自己动手写一个Linux死锁检测器不走内核那种重型方案就把最核心的“用有向图建模-找环”这套逻辑亲手实现一遍。这篇文章就是这个项目的完整记录从图结构怎么设计到环检测算法怎么写再到运行时数据怎么采集、如何验证和排坑全部讲透。如果你对多线程编程有一定基础想知道死锁检测到底是怎么工作的或者单纯想看看一个“检测器”从零到能用的全过程这篇应该对你有用。1. 项目整体设计与思路拆解1.1 为什么我不用现成工具非要手写一个先说结论市面上不是没有死锁检测工具。gdb可以看线程堆栈pstack能打印所有线程的栈甚至fork一个子进程去抓就能大致判断是不是死锁。有些动态检测工具也做得很全比如valgrind的helgrind、ThreadSanitizer它们能跑出很详细的报告。但这些工具都有一个共同问题它们要么是事后分析要么是重编译或重运行很难直接嵌入到一个正在线上运行的服务里。另外还有一个原因我始终觉得死锁检测这个事原理讲起来大家都会说“检测有向图有没有环”但真正动手写的时候就会发现一堆细节比如“环怎么输出成人类能看的路径”“检测器自己要不要加锁”“检测频率多高不会拖垮业务”。这些点看别人文章是看不出来的只有自己写了才懂。所以这个项目的第一目标不是做生产级工具而是把整套思路亲手落地跑通一次完整流程。还有一层考虑是算法选型。死锁检测里经典的做法有几个方向银行家算法属于预防策略要预知每个线程的最大资源需求这对动态系统不现实超时检测效率低且容易误判内核的lockdep是基于锁顺序检查的静态分析非常强但它管的是内核锁拿到用户态场景还要重新设计。最后我选了“有向图环检测”这条路通用性好能够建模任意线程和资源之间的等待关系而且找环的算法和图论里经典问题直接对应逻辑清晰、可控性强适合自己实现。1.2 死锁检测的理论基础从资源分配图说起死锁的四个必要条件是互斥、持有并等待、不可剥夺、循环等待。前三个是资源本身的属性普通互斥锁天然满足最后一个“循环等待”才是检测器的切入点一组线程彼此等待对方持有的资源形成一个闭环。把这个闭环抽象成图就是资源分配图。经典教科书里有两种节点进程节点用矩形表示和资源节点用圆表示。进程到资源的边表示“进程正在请求这个资源”资源到进程的边表示“这个资源已经分配给了该进程”。如果这个有向图里出现了环就说明存在死锁。但在用户态多线程场景里我更愿意用另一种等价建模方式把每个互斥锁也当作节点线程持有锁A、等待锁B就记录一条A-B的边表示“锁A当前被某个线程持有而这个线程正在等待锁B”。这样边直接描述锁之间的依赖关系。如果存在锁A等锁B、锁B又等锁A的情况图里就会出现A-B-A的环这就是死锁。这种模型比教科书模型更直观而且最后输出环的时候可以直接打印“线程T1持有锁A等待锁B线程T2持有锁B等待锁A”定位问题更快。这里我用“锁依赖图”作为核心模型是因为实际工程里“死锁”总是发生在锁与锁之间而不是抽象的进程与资源之间。检测器不关心线程具体在干什么只关心两件事当前每个线程持有了哪些锁、正在等待哪把锁。只要有这两份信息图就能建起来环就能找出来。1.3 整体架构与模块划分整个检测器我分成三个模块数据采集、图构建、环检测。三者职责分明便于单独调试。数据采集模块负责获取“线程-锁”的实时关系也就是线程当前持有哪把锁、正在等待哪把锁。这部分是整个系统里最麻烦的因为用户态拿不到内核的锁等待信息必须靠插桩或在锁API层做手脚。我采用的方案是拦截pthread的锁操作接口维护一张全局的“线程持有锁集合”和“线程等待锁映射”。图构建模块负责把采集到的关系转换成有向图。图的节点有两类线程节点和锁节点。线程节点表示一个正在参与锁竞争的线程锁节点表示一把具体的互斥锁。边的定义我细分成两种检测线程正在等待某把锁时建立 线程节点-锁节点 的边检测到某把锁被某线程持有时建立 锁节点-线程节点 的边。这样一个完整的等待链就能表达成 线程A - 锁B - 线程B - 锁A - 线程A。环检测模块则基于构建好的图用深度优先搜索DFS判断是否存在环并且枚举环上的所有节点。这三个模块可以独立测试先拿构造好的测试数据测环检测确保算法本身没问题再测数据采集确认线程关系记录准确最后联调跑一个真实的死锁用例验证。分模块的好处是出问题的时候能快速定位是采集的锅还是算法的锅。2. 有向图环检测核心算法与数据结构2.1 数据结构如何表示节点和边图结构这块我直接用C语言手写不依赖第三方库。节点对象需要区分类型同时要携带名字方便最后输出报告。typedef enum { NODE_THREAD, NODE_MUTEX } node_type_t; typedef struct graph_node { int id; // 全局唯一ID node_type_t type; // 线程节点还是锁节点 char name[64]; // 线程名或锁地址的十六进制表示 struct edge_node *in_edges; struct edge_node *out_edges; int visited; // 0未访问 1访问中 2已结束 } graph_node_t; typedef struct edge_node { graph_node_t *from; graph_node_t *to; struct edge_node *next; } edge_node_t;这里有个关键设计每个节点维护出边链表和入边链表。出边用于DFS遍历入边用于反向追踪环路径。反向追踪这一手在死锁定位里特别有用。比如检测到环里有一个锁节点我可以顺着入边找到“哪把锁分配给了哪个线程”再顺藤摸瓜把整条等待链打出来比单纯输出“存在环”的提示要有用得多。锁节点的命名直接用锁变量地址转十六进制因为同一个锁在两次采集期间即使内部结构变化地址也能可靠标识它的身份。线程节点用线程ID加线程名多线程服务里线程名能帮我们快速定位到具体业务模块。2.2 环检测算法三色DFS实现图里找环的经典做法是DFS加递归栈。我用的是“三色标记法”白色表示未访问过灰色表示正在访问也就是当前DFS路径上的节点黑色表示已经访问完成且其子树里没有环。#define COLOR_WHITE 0 #define COLOR_GRAY 1 #define COLOR_BLACK 2 int dfs_detect_cycle(graph_node_t *node, graph_node_t **path, int depth) { if (node-visited COLOR_GRAY) { // 遇到灰色节点说明找到了环从该节点到当前路径末尾即为环 printf(cycle: ); for (int i 0; i depth; i) { if (path[i]-id node-id) { for (int j i; j depth; j) { printf(%s - , path[j]-name); } printf(%s\n, node-name); break; } } return 1; } if (node-visited COLOR_BLACK) { return 0; // 子树已经检测过没有环 } node-visited COLOR_GRAY; path[depth] node; edge_node_t *e node-out_edges; while (e) { if (dfs_detect_cycle(e-to, path, depth 1)) { return 1; } e e-next; } node-visited COLOR_BLACK; return 0; }这段代码的逻辑不复杂但有一个地方最容易写错从灰色节点回溯时需要准确输出环的起点和终点。我在代码里是从当前DFS路径里找到那个已经在访问中的节点再从那个节点开始输出到当前节点正好形成一个完整的环形路径。如果直接打印整个path数组会把环之前的一段无关路径也带出来干扰排查。三色的好处在于能够区分“当前正在走的路径”和“已经确认无环的区域”。如果只用0/1标记unvisited/visitedDFS回溯时可能把已经检测过子树的节点当灰色导致重复遍历或者漏检。实际上在朴素DFS里用节点状态做环检测时使用的就是类似思想只是很多图论教材默认用白色、灰色、黑色三种状态讲得更清晰。2.3 不止找一个环如何处理多条死锁链路实际业务里死锁往往不止一条。比如三个线程循环等待或者一条等待链中间嵌套了另外一段环这时候只输出“存在一个环”是不够的。你修复其中一条链后另一个潜伏的死锁还在线上依旧卡死。我的处理方式是迭代检测每次找到一个环先不跳出循环而是把这个环涉及到的边标记为“已处理”继续扫描剩余节点直到图中没有新环为止。因为真实系统里锁的数量是有限的重复几次总能结束。不过这里要小心把环的边去掉以后图的结构可能被破坏原本不在环上的节点也会受影响。更稳妥的方案是每找到一个环就记录路径然后只把“这个环内最合适的边”从图中移除比如某把锁的持有线程可以先释放再重建图重新扫描。虽然麻烦一点但能保证不会漏掉嵌套环。在我实际测试中两个线程AB-BA死锁是最常见的形态。三线程环形等待T1等T2、T2等T3、T3等T1也有但出现概率低很多因为需要三个线程都精确地在错误顺序上加锁。检测器能一次输出所有环最省心毕竟你不知道线上到底埋了几个雷。2.4 拓扑排序方案对比为什么我坚持DFS有些朋友看到“有向图找环”会立刻想到拓扑排序。确实拓扑排序可以判断图中是否有环如果排序结束时还有节点未入队就说明存在环。但这个方案有个致命短板它告诉你有环却不告诉你环在哪。死锁检测器存在的意义不是“检测到死锁”而是“快速定位到是哪几个线程、哪几把锁形成了死锁”否则运维同事拿到消息还是要用gdb一个个看堆栈检测器就退化成报警器了。DFS递归栈方案天然能记录路径定位到环上的每个节点。而且它不需要额外维护入度数数组直接用DFS的递归状态就能完成判断。代价是递归深度受栈大小限制但在用户态锁依赖图里深度通常不超过几十因为一个线程不可能持有几十把锁还在等第几十把锁嵌套深度是有限的。所以我的最终选择是DFS三色标记法作为环检测主力输出精确环路径拓扑排序只作为一句话校验在写完检测器后跑一下快照数据确认“有环”的判断一致。3. 运行时数据采集检测器最难的其实不是算法3.1 两种采集路线插桩拦截与事后剖析环检测算法本身不难真正的难点在于图的数据从哪来你不能读取一个线程的“等待状态”因为用户态根本没有这样的API。可选的做法有几条第一事后剖析用gdb或pstack抓取线程栈人工或脚本解析每把锁的地址和调用关系。优点是实现简单不用动业务代码缺点是滞后。死锁已经发生还要等人工介入效率低。而且部分锁等待发生在代码深处栈解析容易漏信息。第二侵入式插桩业务代码里手动在每次加锁前调用检测器的注册函数把“线程T正在等待锁L”上报。这个方案准确但得改业务代码很多团队不接受。第三动态链接拦截利用Linux的LD_PRELOAD机制在pthread_mutex_lock等函数外面包一层在调用真实函数之前记录等待关系调用完成之后记录持有关系。这是三个方案里最平衡的不需要改业务代码也能做到实时采集。代价是要管理好拦截层自身的数据结构和锁避免“检测器自己把自己检测出死锁”。我最终用的是第三条路线。具体来说我的拦截层维护了一张全局表结构大致如下typedef struct wait_record { pthread_t tid; // 等待者线程 pthread_mutex_t *mutex; // 等待的锁 struct timespec wait_start; // 等待起始时间用于超时辅助判断 } wait_record_t;当拦截层截获某个线程对锁的lock调用时先更新“该线程等待的锁”为当前锁lock成功返回后再把这条等待记录从表里移除并加入到“线程持有锁集合”。这样任何一个时刻我都能回答线程A现在在等锁M线程B正持有锁N。这里有一个需要特别注意的点拦截层自己的全局表也要用锁保护否则检测器在多线程并发下数据都会坏。保护表用的是普通互斥锁但这里有一个经典问题线程在等待业务锁的时候不会持有拦截层的锁只有更新记录的那一刻才短暂持有。我特意把临界区控制在最小范围避免“检测器的锁”参与业务锁的等待链。3.2 LD_PRELOAD拦截的原理与实施细节LD_PRELOAD的原理很简单在动态链接器加载共享库时优先加载指定的.so文件。而pthread系列函数在glibc里本身就是可重定位的符号只要我们在自己的库里提供同名函数动态链接时就会优先绑定到我们的版本然后我们再用dlsym拿到真实函数的地址去调用。#define _GNU_SOURCE #include dlfcn.h #include pthread.h static int (*real_pthread_mutex_lock)(pthread_mutex_t *) NULL; void __attribute__((constructor)) init_hook() { real_pthread_mutex_lock dlsym(RTLD_NEXT, pthread_mutex_lock); } int pthread_mutex_lock(pthread_mutex_t *mutex) { record_wait_start(pthread_self(), mutex); int ret real_pthread_mutex_lock(mutex); record_wait_end(pthread_self(), mutex); record_hold_add(pthread_self(), mutex); return ret; }这里最需要注意的点是record_wait_end必须要在pthread_mutex_lock返回之后调用因为返回成功才说明拿到了锁。但这样做有一个缺陷如果线程阻塞在锁上record_wait_start和record_wait_end之间的时间就是真实等待时间我们记录的是“正在等待”“等待结束”两个时间点这个粒度已经够用。对于pthread_mutex_timedlock我还会记录超时信息方便区分“等待中”和“已经放弃等待”。拦截所有相关的pthread函数也是有必要的。pthread_mutex_lock/unlock是核心但实际工程里还有pthread_rwlock_rdlock、pthread_rwlock_wrlock、pthread_mutex_trylock、pthread_cond_wait。其中pthread_cond_wait比较特殊阻塞期间线程会释放互斥锁被唤醒时又重新获取。这个转换过程如果不处理采集到的持有集合会不准确。我这边做了一点取舍第一版只重点实现mutex相关接口的拦截rwlock和cond后续在扩展里做了补充。对死锁检测来说互斥锁是最常见的死锁源头先把这条链路跑通最重要。3.3 采集频率与一致性问题你可能会问实时记录每把锁的获取释放那检测器什么时候建图扫描最直觉的做法是每次锁状态变化就触发一次扫描。但性能完全不可接受尤其在锁竞争高的服务上每秒可能发生几千次lock/unlock每次都扫描图加DFS业务延迟会被拖到不可用。实际上锁的获取和释放是高频变化而“死锁形成”是一个状态它需要等待链在某个时间点成立。等待链不会在几微秒内一瞬即逝因为死锁的语义就是这些线程都永久阻塞。所以真正合理的策略是周期采样每100毫秒或1秒可配置从全局表中读取一次快照基于快照构建依赖图然后跑环检测。这里就涉及一致性问题采样瞬间某个线程可能正在持有锁A、等待锁B但下一秒它获取了锁B快照里的状态就过期了。会不会误报我用了一个折中方案等待记录里带有等待起始时间只有等待时间超过一个阈值比如30毫秒的边才会被纳入检测。这样既能避开瞬时等待的噪音又能捕捉到真正卡住的线程。实测下来这个方案的效果很好。正常业务的短锁竞争会被阈值滤掉而真正死锁场景下等待时间一定超过阈值建出来的图是稳定而且准确的。4. 完整流程验证从AB-BA死锁到检测报告4.1 构建一个经典的死锁测试用例为了验证检测器我写了一个教科书级的AB-BA死锁场景线程1先锁mutex_A再锁mutex_B线程2先锁mutex_B再锁mutex_A。两个线程同时在中间点交错就会互相等待对方释放自己需要的锁。pthread_mutex_t mutex_A PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex_B PTHREAD_MUTEX_INITIALIZER; void *thread_1(void *arg) { pthread_mutex_lock(mutex_A); usleep(100 * 1000); // 确保线程2拿到B pthread_mutex_lock(mutex_B); // 等线程2释放B死锁就在这里 pthread_mutex_unlock(mutex_B); pthread_mutex_unlock(mutex_A); return NULL; } void *thread_2(void *arg) { pthread_mutex_lock(mutex_B); usleep(100 * 1000); // 确保线程1拿到A pthread_mutex_lock(mutex_A); // 等线程1释放A形成环 pthread_mutex_unlock(mutex_A); pthread_mutex_unlock(mutex_B); return NULL; }这里的usleep非常关键。没有它两个线程大概率不会同时持有对方需要的锁死锁就不会稳定复现。加了这个睡眠窗口线程1拿A、线程2拿B然后互相等对方闭环就建立了。这也是为什么真实系统的死锁难以复现时序差一点点环就形成不了。编译时注意要链接pthreadgcc死锁示例代码时需要加-lpthread。如果用了LD_PRELOAD方式注入检测器还需要确保检测器的.so先编译出来然后通过环境变量LD_PRELOAD加载。4.2 运行检测器看到完整的死锁链路用LD_PRELOAD加载检测器后跑起上面的程序周期采样任务会在后台运行。大约一秒后检测器输出如下[deadlock] cycle detected: thread[140123456789012] holds mutex[0x7f0011223344](A) waiting for mutex[0x7f0011223355](B) thread[140123456789123] holds mutex[0x7f0011223355](B) waiting for mutex[0x7f0011223344](A)这就是我希望看到的效果直接告诉你线程1拿着锁A在等锁B线程2拿着锁B在等锁A。运维同事拿到这条日志不需要再看gdb堆栈直接去代码里搜这两个锁的加锁顺序就能定位问题。为了让输出更直观我还在环路径里加上了“持有”和“等待”的语义标注而不仅仅打印节点名。因为在纯图模型里环上节点的角色需要人工判断但采集模块记录了每个线程持有哪些锁、等待哪些锁所以输出报告时可以直接把这些信息嵌入到环路径的每个节点里。4.3 检测结果验证与误报分析我反复跑了很多次测试程序检测器都能稳定抓住死锁。但我也特意构造了一些“看起来像死锁、其实不是”的场景来验证误报率。第一种线程A等待锁M但锁M马上被释放了等待时间只有1毫秒。由于阈值过滤这条等待边根本不会进入建图阶段不会误报。第二种线程A等待锁M线程B等待锁N但它们互不关联依赖图里根本没有环DFS自然找不到环。第三种线程A持有锁M等待锁N线程B持有锁N等待锁M但A持有的M不是“让B等了很久”的同一把锁而是另一个地址的锁。这种情况下锁依赖图里虽然也有A-N和B-M的边但图中不一定成环需要看具体地址是否对应相同的锁节点。这个点在设计节点命名时就要注意锁节点必须用地址标识不能用名字因为不同作用域里可能定义同名锁变量。实测下来我的检测器只会在“等待链闭合”的时候报警而且能唯一确定到锁的地址。误报率基本为零前提是采集层的记录准确。5. 实操过程与踩坑记录5.1 数据采集层的一个大坑检测器自身死锁我在第一版实现里踩过一个特别经典的坑检测器的全局表用了一把普通的pthread互斥锁保护。而拦截层在处理“线程正在等待锁M”时需要先更新全局表再调用真实的pthread_mutex_lock。结果在某种极端顺序下线程1持有了全局表锁又去等业务锁M线程2持有了业务锁M又来等全局表锁。检测器的锁和业务锁互相成了环于是“检测死锁的工具”自己也死锁了。这个问题其实在理论层面很常见但只有自己实现的时候才会真正记住采集层必须避免在持有自身锁的情况下调用任何可能阻塞的业务锁操作。我的修复方式是把更新全局表逻辑放在调用真实lock之前并且保证在这段代码里绝对不调用任何pthread锁函数只做纯内存的哈希表插入操作。这样采集层持有自己锁的时间只有几十纳秒且不参与任何外部锁竞争从根本上破坏结成环的可能性。5.2 删除边时的悬垂指针问题图中节点和边都是动态分配的。线程退出后线程节点理论上应该从图中删除。但线程节点可能正处于某条环路径上或者锁节点还有指向它的出边。直接删除节点会导致悬垂指针下一次DFS访问空地址直接崩溃。我采取的策略是不做即时物理删除而是给节点打一个“已失效”标记。每次重建图时上一轮快照的数据整体清空重新按新快照构建。虽然有点浪费内存但胜在简单可靠。死锁检测的扫描频率是秒级的图规模也就几十到几百个节点清理开销完全可以忽略。这里也体现了一个设计取舍死锁检测器不是实时性要求很高的系统不需要增量更新图结构每次全量重建反而能避免一堆边界问题。5.3 锁的地址复用与脏节点还有个坑一把锁被销毁后另一把新锁可能被分配在同一个地址上。如果不清理旧节点检测报告里会出现“线程等待锁0x7f0011223344”但代码里这个地址对应的锁已经换了一把。我处理的办法是给每个锁节点记录“创建时间”或“代数”。每次采集时发现锁的地址相同但绑定的线程上下文变化异常比如之前是等锁状态现在是空闲状态就判断为旧锁已销毁。这个逻辑虽然不完美但能挡掉大部分脏数据问题。从工程角度看这类边界情况如果不管会导致偶发的错误报警。死锁检测器一旦误报运维就会失去信任所以这类细节宁多勿少。5.4 性能开销每100ms采样一次够用吗我最初设定的是每100毫秒采样一次性能开销很低十几MB内存就能跑得很稳。实际测试发现100毫秒对死锁检测来说太“灵敏”了有些瞬时的高延迟锁竞争会被采集进来形成短边。把阈值调高到30毫秒等待时间后效果更稳。后面我又加了一个自适应策略如果没有检测到环采样间隔逐步增大到1秒一旦发现有等待边持续超过阈值就缩小采样间隔到200毫秒。这样可以兼顾“日常低开销”和“死锁发生时快速报警”两个需求。CPU开销方面我统计了一下一次全量建图加DFS扫描平均耗时不到1毫秒扫描10万节点规模的图也不会有明显延迟。所以性能不是瓶颈真正的瓶颈在于采集层怎样才能不干扰业务锁的正常竞争。6. 常见问题与排查技巧实录6.1 为什么检测器对自旋锁不生效这个是最常被问到的。pthread自旋锁pthread_spin_lock不通过pthread_mutex_lock接口而是spin_lock系列函数。如果只拦截了mutex系列自旋锁死锁是检测不到的。解决办法也很直接在拦截层里同样包一层pthread_spin_lock/unlock用同样的逻辑记录等待关系。但自旋锁有个特点等待时CPU是自旋空转的采集层拿到的等待开始时间非常短如果阈值设高了会漏检设低了又容易误报。我的建议是给自旋锁单独设一个更小的等待阈值并且配合CPU占用率一起判断。6.2 递归锁为什么检测不出环递归锁PTHREAD_MUTEX_RECURSIVE的特点是同一个线程可以重复加锁。如果业务代码里错误地让线程A持有递归锁后又等它自己这个等待不会阻塞因为递归锁允许同一线程重入所以根本不会形成“等待”状态。从采集层看lock调用会立即返回等待时间几乎为零边都建立不起来。这类问题本质上是逻辑错误不是死锁检测器管不了也正常。6.3 条件变量导致的假死锁怎么识别pthread_cond_wait会让线程释放互斥锁并挂起等待。如果另一个线程signal丢了线程可能永远阻塞在cond_wait里。从图上看线程没有在等待任何锁而锁已经被释放没有环。所以这类问题不会被我这个方向的检测器报警。实际运维时这类问题往往表现为“线程阻塞在cond_wait但锁是空闲的”用pstack就能看出来。死锁检测器只解决“锁与锁之间循环等待”其他阻塞问题需要配合其他工具一起用。6.4 检测器本身会影响死锁时序吗会。任何hook方式都会改变函数调用路径增加一些微秒或纳秒级的延迟导致原本会稳定复现的死锁变得时灵时不灵。我在测试时发现加了检测器后有些场景反而“卡不住”了因为线程执行的节奏被采集代码拉偏。这时可以适当调大测试代码里的usleep窗口或者用更确定的方式触发死锁比如写死两个线程各自sleep固定时间后再锁对方。这个现象也说明一个道理因为检测器可能扰动时序所以它适合做“发现问题后的定位器”和“线上一旦形成死锁就报警”的监控器但不太适合用来复现那些偶发的时序问题。6.5 如何把检测器接入线上服务且不影响业务我最终的接入方式是做成一个独立的.so通过LD_PRELOAD注入目标进程。线上接入时注意两点第一先在一台低流量实例上灰度运行观察内存和CPU增量第二日志输出走异步通道比如写到独立的日志文件或syslog不阻塞业务线程。另外检测器自身必须支持动态开关通过配置文件或信号控制采样启停。因为一旦业务压测或大促期间任何额外开销都不受欢迎。接入后的实际体验是平时几乎无感系统稳定运行好多天都扫不到环但只要业务代码里存在锁顺序不统一的地方早晚有一天会触发报警。与其在故障复盘时对着堆栈人肉分析不如提前让图找环替你把活儿干了。写在最后我自己的几点体会这个项目从头到尾跑完之后我最大的感受是找环的算法只占20%的工作量剩下80%都在和数据采集、边界条件作斗争。判断有向图有没有环是图论里最基础的问题之一但要把一个线上多线程服务的锁竞争关系准确、低开销、不干扰业务地采集下来才是真正见功夫的地方。如果你也想自己实现一遍我的建议是别一上来就追求生产级功能先做一个能在测试程序里稳定报出AB-BA死锁的最小版本再逐步加多线程场景、rwlock、自旋锁最后再接线上。每走一步你都会遇到新的边界问题这些问题的解法就是你这个检测器项目的真正积累。最后分享一个小技巧我在检测出环之后会把当时的完整依赖图dump成一份文件包含每个锁的地址、每个线程的开始等待时间、持有锁的列表。这样就算这次误报了或者当时没处理事后也能拿着文件离线复现和分析。检测器不仅要能“报警”还要把现场保存下来这才是它最有价值的地方。
阅读完成 · 觉得有帮助?