golang自带的死锁检测并非银弹

news/2025/1/7 6:29:58/文章来源:https://www.cnblogs.com/apocelipes/p/18652971

网上总是能看到有人说go自带了死锁检测,只要有死锁发生runtime就能检测到并及时报错退出,因此go不会被死锁问题困扰。

这说明了口口相传知识的有效性是日常值得怀疑的,同时也再一次证明了没有银弹这句话的含金量。

这个说法的杀伤力在于它虽然不对,但也不是全错,真真假假很容易让人失去判断力。

死锁检测失灵

死锁我就不多解释了,我们先来看个简单例子:

package mainimport ("fmt"
)func main() {c := make(chan int, 1)fmt.Println(<-c)
}

这段代码会触发golang的死锁报错:

fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive]:
main.main()/tmp/deadlock.go:9 +0x32
exit status 2

这个例子为啥锁死了,因为没人给chan发数据,所以接收端永久阻塞在接收操作上了。

这说明了go确实有死锁检测。只不过你要是觉得它什么样的死锁都检测到那就大错特错了:

package mainimport ("fmt""time"
)func main() {c := make(chan int, 1)for {go func() {fmt.Println(<-c)}()time.Sleep(10 * time.Millisecond)}
}

根据示例1我们可以知道如果一个chan没有发送者,那么所有的接收者都会阻塞,在我们的例子里这些协程是永久阻塞的,理论上应该会被检测到然后报错。

遗憾的是这个程序会持续运行下去,直到内存耗尽为止:

deadlock1

死锁检测是有足够的时间执行的,因为10毫秒虽然对人类来说短的可以忽略但对golang运行时来说相当漫长,而且我们在不停创建协程,满足所有触发检测的条件,具体条件后面会细说。

从实验对照的角度来说,这时合理的猜测应该是会不会主协程被特殊处理了,因为上面的例子里子协程全部死锁,但主协程并没有。所以我们再次进行测试:

package mainimport ("fmt""time"
)func main() {c := make(chan int, 1)go func() {for {fmt.Println("Hello from child.")time.Sleep(100 * time.Millisecond)}}()<-c
}

这段代码同样不会报错,程序会持续输出Hello直到你手动终止进程或者关机为止。这正说明了runtime不会在死锁检测上特殊对待主协程。

看上去go的死锁检测时常“失灵”,这是一件很恐怖的事情,尤其是在你信了文章开头那个说法在代码里放飞自我认为只要没报错就是没问题之后。

go的死锁检测到底检测了什么

说这是“失灵”其实有失偏颇,上面的现象解释起来其实很简单,三两段话就能说明白。

首先我们可以把go里的协程分为两大类,一类是runtime自己的协程,包括sysmon和gc;另一类是用户创建的协程,包括用户自己创建的,用户使用的第三方库/标准库创建的所有协程。我们暂且管后者叫“用户协程”。这只是很粗糙的分类,实际的代码中有不少出入,不过作为抽象概率帮助理解是没问题的。死锁检测针对的就是“用户协程”。

知道了检测范围,我们还需要知道检测内容——换句话说,什么情况下能判断一组协程死锁了?理想中当然是检测到一组协程循环等待某些条件或者阻塞在一些永远不会有数据的chan上。现实是go只检测这些:

  1. 是否有协程处于运行状态,包括并未实际运行在等待调度的“可运行”用户协程;
  2. 没有上述条件的协程就检测是否还有未触发的定时器;
  3. 都不满足才会触发死锁报错并终止程序。

检测的时机其实也是有些反直觉的,go只在创建/退出操作系统级别的线程、这些线程变为空闲状态时、sysmon检测到程序处于空闲时才会执行死锁检测。也就是说,触发检测其实和操作系统线程相关性更强而不是和goroutine。

所以,只要还有一个协程能继续运行,哪怕其他99999个协程都锁地死死得,go的死锁检测依然不会报错(更正确的说法是只要还有一个能继续运行的系统级线程,那就不算死锁,这样才能解释为什么有还未触发的定时器以及在等待系统调用也不算死锁)。这样解释了为什么示例2和3都能运行,因为2中主协程能正常运行,3中子协程能正常运行,因此其他的协程锁死了也不会报错。

检测还有两个例外:

  1. cgo管不了,因此go程序调用的c/c++代码的线程里锁死了go这边也没有办法
  2. 把go代码编译成c库之后死锁检测会主动关闭,因为如果c/c++代码没调用库里的函数的话,那就只有runtime协程存在,这时候检测会发现根本没有用户协程,这种检测没有意义。所以在这种情况下哪怕go代码真的全部死锁了也不会检测到。

有人估计觉得这是bug或者设计失误要急着去提issue了,但这不是bug!这只是看待问题的方式不同。

go采用的做法,比较正式的描述是“在期望时间内程序的运行是否能取得进展”,这里的进展当然是指的是否在运行或者有定时器/io要处理。以此标准只要还有用户协程能动,那说明“整个程序”并没有死锁——go里判断要不要触发死锁报错是以整个程序作为基准的,而我们通常的判断基准是所有用户协程都能在“期望时间内获得进展”才是没有问题。后者的要求更严格。

而且满足后者的检测实现起来很复杂,预计也会花费非常多的计算资源,从维护和运行性能的角度来说想做也不是很现实。所以go选择了前者,前者虽然不能处理所有的问题,但仍然能在早期阶段防止出现一部分死锁问题。

然而把死锁检测当成万金油保险丝的人就要倒霉了:

  1. 现实的项目中出现一次性锁死整个程序的情况其实是比较少的,更多的时间是像例子中那样一部分协程锁死;
  2. 锁死的协程除了不能继续运行之外,还会造成协程泄漏,更要命的是协程持有的对象都不会释放,所以还伴随着内存泄漏;
  3. 在一些程序里系统的一部分锁死了可能在短时间内影响不到其他部分,在web应用中很常见,这会让问题发生难以察觉,往往当你意识到出问题时整个程序已经到万劫不复的状态了。

所以死锁检测只能偶尔帮你一次,并不能当成救命稻草用。

死锁检测的源代码在"src/runtime/proc.go"的checkdead函数里,感兴趣的可以自行把玩。

怎么检测死锁

既然报错不能料理所有情况,我们还能借助哪些工具定位是否有死锁发生呢?

其实没啥好办法,下面每一种方案都需要经验以及结合实际代码才能判断出结果。

第一种是观察协程数量或者内存占用是否异常。比如你的程序正常需要1000个协程,那么2千个协程也许不是出问题了,但出现2万个协程那肯定是不对劲的。内存占用同理。

这些数据很好获取,不管是go自带的pprof还是trace,或者是第三方的性能监控,都能很轻松的探测到异常。难的是如何定位具体的问题。不过这节说的是如何发现死锁,所以出现上述异常后把可能存在死锁放进排查方向里也就够了。因为协程泄漏虽然不一定都是死锁造成的,但死锁最直接的表现就是协程泄漏。

方案1的缺点也很明显,如果死锁的协程数量固定,或者产生死锁协程的速度很慢,那么监控数据上很难发现问题。我们也不可能简单地用服务没响应了来判断是不是出了死锁,无响应的原因实在是太多了。

此外uber开发的用于检测协程泄漏的库goleak也可以帮上一些忙,不过缺点是一样的。

第二种是用go trace或者调试器dlv看运行时的协程栈。如果栈里出现很多lock类函数或者chan收发函数,那么存在死锁协程的概率是比较大的。最重要的是要看不同协程的调用栈里是否存在循环依赖或者交叉加锁的情况。

方案2的缺点也很明显,第一个是需要在程序运行时获取调用栈信息,这会影响程序的性能,协程越多影响越明显;第二是分析调用栈现在没啥好的自动化工具往往得程序员自己上阵,如果协程数目巨大的话分析会变得极度困难。

而且调用栈里lock函数多不代表一定有死锁,也可能只是锁竞争激烈而已。

最后一种方案是借助go trace工具,trace里有个叫block profile的,这个可以统计哪些函数被阻塞住了。如果看到里面有大量的lock、select相关函数、chan相关操作函数,那么死锁的可能性很大。

但和方案2一样,方案3依然不能100%确定存在问题,还是要结合实际代码做分析。而且trace只能分析某个时间段内的程序运行情况,如果你的程序死锁问题是偶发的,那么很可能抓几百次trace数据都不一定能抓到案发现场。

最后结论就是没有银弹。所以与其期待有个万能检测器不如写代码的时候就提前预防问题发生。

总结

我之所以写这篇文章是因为很多golang的布道师居然会以golang有死锁检测所以能避免死锁错误为卖点宣传go语言,信以为真的go用户也不少。稍加实验就能证伪的说法如今依然大行其道,令人感叹。

软件行业是很难出现银弹的,因此多动手检验才能少踩坑早下班。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/864268.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025多校冲刺省选模拟赛2

2025多校冲刺省选模拟赛2\(T1\) A. aw \(10pts/20pts\)部分分\(10 \sim 20pts\) :枚举每一种定向方案,略带卡常。点击查看代码 const int p=998244353; struct node {int nxt,to; }e[200010]; int head[100010],dis[1010][1010],a[100010],b[100010],g[2][100010],cnt=0; b…

jamovi 2.6 (Linux, macOS, Windows) - 统计软件

jamovi 2.6 (Linux, macOS, Windows) - 统计软件jamovi 2.6 (Linux, macOS, Windows) - 统计软件 open statistical software 请访问原文直链:https://sysin.org/blog/jamovi/ 查看最新版。原创作品,转载请保留出处。 作者主页:sysin.orgjamovi适用于桌面和云的开放式统计软…

读数据保护:工作负载的可恢复性26商用数据备份方案

商用数据备份方案1. 备份简史 1.1. 20世纪80年代中期大家都还没有意识到,运行着商用UNIX操作系统的大型工作环境里,应该配备一款商用的备份软件或某种自动的磁带系统 1.2. 1993年备份工作全都是通过shell脚本与cron job形式的计划任务来实现的1.2.1. 脚本总是假定服务器中需要…

OpenCV和OpenVX有什么联系和区别

OpenCV和OpenVX有什么联系和区别 联系和区别是:OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库。OpenVX 实现了跨平台加速处理,OpenVX在嵌入式和实时性系统中,可以更好地发挥它的优势,在某些场合配合OpenCV的强大功能,可以实现更好的效果。…

SPIR-V生态系统概述

SPIR-V生态系统SPIR-V生态系统,如图1-42所示。图1-42 SPIR-V生态系统 1.4.2 OpenVX路线图OpenVX路线图,如图1-43所示。图1-43 OpenVX路线图 OpenVX跨供应商视觉与推理 基于图形的高级抽象实现可移植、高效的视觉处理。 1)处理器供应商创建、优化和发布优化OpenVX的驱动程序。…

4本书推荐《智能汽车传感器:原理设计应用》、《AI芯片开发核心技术详解》、《TVM编译器原理与实践》、《LLVM编译器原理与实践》

4本书推荐《AI芯片开发核心技术详解》、《智能汽车传感器:原理设计应用》、《TVM编译器原理与实践》、《LLVM编译器原理与实践》由清华大学出版社资深编辑赵佳霓老师策划编辑的新书《AI芯片开发核心技术详解》已经出版,京东、淘宝天猫、当当等网上,相应陆陆续续可以购买。该…

Easysearch 可搜索快照功能,看这篇就够了

可搜索快照功能改变了我们对备份数据的查询方式。以往要查询备份数据时,要先找到备份数据所在的快照,然后在一个合适的环境中恢复快照,最后再发起请求查询数据。这个处理路径很长,而且很消耗时间。可搜索快照功能将大大简化该处理路径,节约时间。 角色设置 相信你对节点角…

基本共射极放大电路的分析

静态分析利用直流通路求Q点(静态工作点)\[I_{BQ}=\frac{V_{BB}-V_{BEQ}}{R_{b}} \]一般硅管\(V_{BE}=0.7V\),锗管\(V_{BE}=0.2V\),\(\beta\)已知 \[I_{CQ}=\beta I_{BQ} \]\[V_{CEQ}=(\frac{V_{CC}-I_{CQ}}{R_{c}}-I_{CQ})R_L \]动态分析交流通路分析画小信号等效模型\[r_{…

使用扣子实现营销获客套电机器人-工作流+多维表格+飞书机器人

V+: llike620 就是利用扣子的工作流,实现简单的获取线索机器人,然后对接在抖音音私信上 主要用于某汽车贴膜产品的获客,先获取车型,再获取联系方式增加了状态机制,不能让对方跳过业务流程新线索存入飞书多维表格,并通过飞书机器人进行通知 十年开发经验程序员,离职全心…

Qt配置和功能使用说明

1、执行qmake qmake 是 Qt 提供的一个构建工具,用于简化跨平台 Qt 应用的构建流程。它通过读取 .pro 文件生成适合目标平台的构建文件,例如 Makefile(windows & linux) 或 Visual Studio 工程文件(windows)2、Shadow Build(影子构建) Shadow Build 是 Qt Creator 提…

静态工作点对波形失真的影响

一、截止失真原因:当静态工作点设置过低,即\(I_{BQ}\)过小,\(V_{CEQ}\)过大时,输入信号的负半周可能会使晶体管进入截止区。 现象:输出波形的正半周被削顶,即正半周顶部被“切掉”一部分,这是因为在截止区,晶体管的集电极电流\(i_C\)几乎为零,不能跟随输入信号的变化而…

基本共射极放大电路

以下是关于基本共射极放大电路的详细介绍: 电路结构由晶体管(通常是BJT)、直流电源\(V_{CC}\)、基极偏置电阻\(R_b\)、集电极负载电阻\(R_c\)、输入电容\(C_1\)、输出电容\(C_2\)以及输入信号源\(v_s\)和负载电阻\(R_L\)组成。工作原理直流偏置:通过\(R_b\)和\(V_{CC}\)为晶…