golang自带的死锁检测并非银弹
网上总是能看到有人说go自带了死锁检测,只要有死锁发生runtime就能检测到并及时报错退出,因此go不会被死锁问题困扰。
这说明了口口相传知识的有效性是日常值得怀疑的,同时也再一次证明了没有银弹这句话的含金量。
这个说法的杀伤力在于它虽然不对,但也不是全错,真真假假很容易让人失去判断力。
死锁检测失灵
死锁我就不多解释了,我们先来看个简单例子:
package main
import (
"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 main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 1)
for {
go func() {
fmt.Println(<-c)
}()
time.Sleep(10 * time.Millisecond)
}
}
根据示例1我们可以知道如果一个chan没有发送者,那么所有的接收者都会阻塞,在我们的例子里这些协程是永久阻塞的,理论上应该会被检测到然后报错。
遗憾的是这个程序会持续运行下去,直到内存耗尽为止:

死锁检测是有足够的时间执行的,因为10毫秒虽然对人类来说短的可以忽略但对golang运行时来说相当漫长,而且我们在不停创建协程,满足所有触发检测的条件,具体条件后面会细说。
从实验对照的角度来说,这时合理的猜测应该是会不会主协程被特殊处理了,因为上面的例子里子协程全部死锁,但主协程并没有。所以我们再次进行测试:
package main
import (
"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只检测这些:
- 是否有协程处于运行状态,包括并未实际运行在等待调度的“可运行”用户协程;
- 没有上述条件的协程就检测是否还有未触发的定时器;
- 都不满足才会触发死锁报错并终止程序。
检测的时机其实也是有些反直觉的,go只在创建/退出操作系统级别的线程、这些线程变为空闲状态时、sysmon检测到程序处于空闲时才会执行死锁检测。也就是说,触发检测其实和操作系统线程相关性更强而不是和goroutine。
所以,只要还有一个协程能继续运行,哪怕其他99999个协程都锁地死死得,go的死锁检测依然不会报错(更正确的说法是只要还有一个能继续运行的系统级线程,那就不算死锁,这样才能解释为什么有还未触发的定时器以及在等待系统调用也不算死锁)。这样解释了为什么示例2和3都能运行,因为2中主协程能正常运行,3中子协程能正常运行,因此其他的协程锁死了也不会报错。
检测还有两个例外:
- cgo管不了,因此go程序调用的c/c++代码的线程里锁死了go这边也没有办法
- 把go代码编译成c库之后死锁检测会主动关闭,因为如果c/c++代码没调用库里的函数的话,那就只有runtime协程存在,这时候检测会发现根本没有用户协程,这种检测没有意义。所以在这种情况下哪怕go代码真的全部死锁了也不会检测到。
有人估计觉得这是bug或者设计失误要急着去提issue了,但这不是bug!这只是看待问题的方式不同。
go采用的做法,比较正式的描述是“在期望时间内程序的运行是否能取得进展”,这里的进展当然是指的是否在运行或者有定时器/io要处理。以此标准只要还有用户协程能动,那说明“整个程序”并没有死锁——go里判断要不要触发死锁报错是以整个程序作为基准的,而我们通常的判断基准是所有用户协程都能在“期望时间内获得进展”才是没有问题。后者的要求更严格。
而且满足后者的检测实现起来很复杂,预计也会花费非常多的计算资源,从维护和运行性能的角度来说想做也不是很现实。所以go选择了前者,前者虽然不能处理所有的问题,但仍然能在早期阶段防止出现一部分死锁问题。
然而把死锁检测当成万金油保险丝的人就要倒霉了:
- 现实的项目中出现一次性锁死整个程序的情况其实是比较少的,更多的时间是像例子中那样一部分协程锁死;
- 锁死的协程除了不能继续运行之外,还会造成协程泄漏,更要命的是协程持有的对象都不会释放,所以还伴随着内存泄漏;
- 在一些程序里系统的一部分锁死了可能在短时间内影响不到其他部分,在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用户也不少。稍加实验就能证伪的说法如今依然大行其道,令人感叹。
软件行业是很难出现银弹的,因此多动手检验才能少踩坑早下班。
golang自带的死锁检测并非银弹的更多相关文章
- MySQL学习(四)死锁及死锁检测
文章图片来自参考资料 MySQL 的锁 根据加锁的范围,MySQL里面的锁大致可以分成全局锁.表级锁和行锁三类.我们需要明白锁的服务是为了数据统一,或者说是事务,数据隔离. 全局锁 全 ...
- linux死锁检测的一种思路【转】
转自:http://www.cnblogs.com/mumuxinfei/p/4365697.html 前言: 上一篇博文讲述了pstack的使用和原理. 和jstack一样, pstack能获取进 ...
- 为什么MySQL死锁检测会严重降低TPS
在大量的客户端,更新数据表的同一行时,会造成数据库的吞吐量大幅降低. 很多数据库的前辈和同行分别通过实验和源码的方法,定位到了罪魁祸首----MySQL死锁检测 实验方式:http://blog.cs ...
- linux死锁检测的一种思路
前言: 上一篇博文讲述了pstack的使用和原理. 和jstack一样, pstack能获取进程的线程堆栈快照, 方便检验和性能评估. 但jstack功能更加的强大, 它能对潜在的死锁予以提示, 而p ...
- 14.3.5.2 Deadlock Detection and Rollback 死锁检测和回滚:
14.3.5.2 Deadlock Detection and Rollback 死锁检测和回滚: InnoDB 自动检查四艘,回滚一个事务或者事务来打破死锁. InnoDB 试图选择小的事务来回滚, ...
- UNIX环境高级编程——记录上锁(fcntl函数)以及死锁检测
一.记录锁 record locking 功能:当一个进程正在读或修改文件的某个部分时,它可以阻止其它进程修改同一文件区. 字节范围锁 byte-range locking 二.历史 flock函数, ...
- Linux死锁检测-Lockdep
关键词:LockDep.spinlock.mutex. lockdep是内核提供协助发现死锁问题的功能. 本文首先介绍何为lockdep,然后如何在内核使能lockdep,并简单分析内核lockdep ...
- 谈谈MySQL死锁之二 死锁检测和处理源码分析
这一篇主要是通过一个实验来进行描述,过程是比较枯燥的. 实验准备 create table test_lock(id int auto_increment primary key ,stock int ...
- 当前数据库普遍使用wait-for graph等待图来进行死锁检测
当前数据库普遍使用wait-for graph等待图来进行死锁检测 较超时机制,这是一种更主动的死锁检测方式,innodb引擎也采用wait-for graph SQL Server也使用wait-f ...
- MySQL死锁检测和回滚
最近碰到“TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACT ...
随机推荐
- CentOS8安装RabbitMQ3.8.16
之前安装过旧版的RabbitMQ和Erlang,先卸载. ①:卸载RabbitMQ /sbin/service rabbitmq-server stop yum list | grep rabbitm ...
- 史上最全 Terraform 入门教程,助你无坑入门!
在云计算的浪潮中,基础设施管理变得越来越复杂.如何高效地配置和管理云资源,成为了每个开发者和运维工程师必须面对的挑战.Terraform,作为一种强大的基础设施即代码(IaC)工具,为我们提供了一种简 ...
- MyBatis-Plus条件构造器:构建安全、高效的数据库查询
一.关于条件构造器(Wrapper) 1.1 简介 MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件.Wrapper 类允许开发者以链式调用的方式构 ...
- 5.6 Linux Vim撤销和恢复撤销快捷键
使用 Vim 编辑文件内容时,经常会有如下 2 种需求: 对文件内容做了修改之后,却发现整个修改过程是错误或者没有必要的,想将文件恢复到修改之前的样子. 将文件内容恢复之后,经过仔细考虑,又感觉还是刚 ...
- 3.19 Linux命令的执行过程是怎样的?(新手必读)
前面讲过,在 Linux 系统中"一切皆文件",Linux 命令也不例外.那么,当编辑完成 Linux 命令并回车后,系统底层到底发生了什么事情呢? 简单来说,Linux 命令的执 ...
- 3. jenkins的管理
1. jenkins的插件管理 Jenkins本身不提供很多功能,我们可以通过使用插件来满足我们的使用.例如从Gitlab拉取代码,使用Maven构建项目等功能需要依靠插件完成.接下来演示如何下载 ...
- 剖析Air724UG的硬件设计,有大发现?02篇
3.8 I2C 管脚名 类型 序号 电压域 描述 I2C2_SCL IO 32 V_GLOBAL_1V8 I2C2 时钟信号,用作 I2C 时需外加 1.8V 上拉 I2C2_SDA IO 31 ...
- 从0搭建一个FIFO模块-02(系统架构)
一.异步FIFO需要注意的问题 所谓异步FIFO,指的是写时钟与读时钟可以不同步,读时钟可以比写时钟快,反之亦然.思考一下,这样会直接地造成两个问题: 由于异步FIFO的基本存储单元是双端口RAM,因 ...
- [python] Python异步编程库asyncio使用指北
Python的asyncio模块提供了基于协程(coroutines)的异步编程(asynchronous programming)模型.作为一种高效的编程范式,异步编程允许多个轻量级任务并发执行,且 ...
- Tensorflow/Keras、Pytorch 杂记
Tensorflow/Keras 直接从文件生成图片数据 ImageDataGenerator,循环生成图片,在重复生成图片之前,会把所有图片都遍历一遍.而且如果图片总量不是生成批量的倍数的话,在生成 ...