网上总是能看到有人说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只检测这些:

  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用户也不少。稍加实验就能证伪的说法如今依然大行其道,令人感叹。

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

golang自带的死锁检测并非银弹的更多相关文章

  1. MySQL学习(四)死锁及死锁检测

    文章图片来自参考资料 MySQL 的锁     根据加锁的范围,MySQL里面的锁大致可以分成全局锁.表级锁和行锁三类.我们需要明白锁的服务是为了数据统一,或者说是事务,数据隔离. 全局锁     全 ...

  2. linux死锁检测的一种思路【转】

    转自:http://www.cnblogs.com/mumuxinfei/p/4365697.html 前言:  上一篇博文讲述了pstack的使用和原理. 和jstack一样, pstack能获取进 ...

  3. 为什么MySQL死锁检测会严重降低TPS

    在大量的客户端,更新数据表的同一行时,会造成数据库的吞吐量大幅降低. 很多数据库的前辈和同行分别通过实验和源码的方法,定位到了罪魁祸首----MySQL死锁检测 实验方式:http://blog.cs ...

  4. linux死锁检测的一种思路

    前言: 上一篇博文讲述了pstack的使用和原理. 和jstack一样, pstack能获取进程的线程堆栈快照, 方便检验和性能评估. 但jstack功能更加的强大, 它能对潜在的死锁予以提示, 而p ...

  5. 14.3.5.2 Deadlock Detection and Rollback 死锁检测和回滚:

    14.3.5.2 Deadlock Detection and Rollback 死锁检测和回滚: InnoDB 自动检查四艘,回滚一个事务或者事务来打破死锁. InnoDB 试图选择小的事务来回滚, ...

  6. UNIX环境高级编程——记录上锁(fcntl函数)以及死锁检测

    一.记录锁 record locking 功能:当一个进程正在读或修改文件的某个部分时,它可以阻止其它进程修改同一文件区. 字节范围锁 byte-range locking 二.历史 flock函数, ...

  7. Linux死锁检测-Lockdep

    关键词:LockDep.spinlock.mutex. lockdep是内核提供协助发现死锁问题的功能. 本文首先介绍何为lockdep,然后如何在内核使能lockdep,并简单分析内核lockdep ...

  8. 谈谈MySQL死锁之二 死锁检测和处理源码分析

    这一篇主要是通过一个实验来进行描述,过程是比较枯燥的. 实验准备 create table test_lock(id int auto_increment primary key ,stock int ...

  9. 当前数据库普遍使用wait-for graph等待图来进行死锁检测

    当前数据库普遍使用wait-for graph等待图来进行死锁检测 较超时机制,这是一种更主动的死锁检测方式,innodb引擎也采用wait-for graph SQL Server也使用wait-f ...

  10. MySQL死锁检测和回滚

    最近碰到“TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACT ...

随机推荐

  1. 欢迎来到IoT解忧杂货铺

    这是一间特殊的杂货铺 门面不大,却包罗万物 如果你也遇到一些烦恼 欢迎来到,IoT解忧杂货铺 解忧秘方·工业 厂里的几十台设备真让人头疼 协议种类太多太复杂 设备没法全联网 产线故障了也不知道 自己出 ...

  2. PHP实现断点续传

    解释 业务上要求对资源文件进行加密,遂实现通过php接口调用,修改header头,传输流的方式. 测试中,在苹果手机上,如果文件过大(大概10M以上),会主动调用多次接口.此时如果不使用断点续传的方式 ...

  3. 哈希(C语言)

    文章目录 1.数据结构--哈希表 1.1哈希表的工作原理 1.2哈希表的代码实现 2.哈希算法 2.1 哈希算法介绍 2.2C语言实现示例 本文介绍一个常用的算法--哈希算法,哈希算法依赖于哈希表来实 ...

  4. python项目实战——一元线性回归预测模型

    文章目录 1.一元线性回归简介 2.环境准备 3.数据准备 4.可视化数据 5.构建线性回归模型 在数据科学领域,预测分析是一项核心技能.一元线性回归作为预测分析的基石,能够帮助我们理解一个自变量如何 ...

  5. Go下载依赖包失败的解决方法

    jenkins构建job,执行go test命令报如下图的timeout错误 从报错信息中可以看出是因为下载依赖包超时 解决方法: go env查看go环境变量 GOSUMDB=sum.golang. ...

  6. 3.5 Linux命令行下如何识别文件类型?

    对于第一次使用 Linux 命令行的用户,可能真的搞不清楚哪个是文件,哪个是目录,究其原因是很难直接通过名字看出来目录和文件的区别. 虽然从名称上不容易分辨,但是可以从颜色上进行区分.一般情况下,Li ...

  7. 开源 - Ideal库 - 常用枚举扩展方法(二)

    书接上回,今天继续和大家享一些关于枚举操作相关的常用扩展方法. 今天主要分享通过枚举值转换成枚举.枚举名称以及枚举描述相关实现. 我们首先修改一下上一篇定义用来测试的正常枚举,新增一个枚举项,代码如下 ...

  8. 高性能计算-openmp-多线程缓存一致性(9)

    1. 背景介绍 L1 L2 cache是单核独享,L3是多核共享.如果多线程访问共享一维数组的连续元素,先读入第一个线程的L1 缓存中,其他线程访问缓存不命中需要加载,并且数据的更改后,标记为脏数据, ...

  9. Transformers包使用记录

    Transformers是著名的深度学习预训练模型集成库,包含NLP模型最多,CV等其他领域也有,支持预训练模型的快速使用和魔改,并且模型可以快速在不同的深度学习框架间(Pytorch/Tensorf ...

  10. pydotplus使用

    pydotplus是别的语言嫁接到python里面的,所以绘制要传入字符串形式表示的结构,而没有python的结构对象直接用来画.代码如下: import pydotplus as pdp graph ...