最近看了一篇文章,如何定位 golang 进程 hang 死的 bug,里面有这样一段代码:
|
|
运行,会发现打印一会儿数字后停了,我们执行
|
|
程序卡死。关于程序挂在哪里借助dlv
是很好定位的:
|
|
进去之后运行程序,打印停止进入卡死状态,我们执行ctrl C
,dlv
会显示断开的地方:
|
|
但是我还是不明白,不明白的地方主要是因为:
- 我又看了两篇文章Goroutine调度实例简要分析和也谈goroutine调度器,是同一位作者Tony Bai写的,写得非常好。第二篇文章解释了goroutine的调度和cpu数量的关系(不多加解释,建议大家看看),我的mac是双核四线程(这里不明白的同学自行google cpu 超线程),go版本是1.9,理论上讲可以跑4个goroutine而不用考虑死循环,一个死循环最多把一个cpu打死,上面的代码中只有3个goroutine,而且他们看上去都挂住了。
- 上面说的理论上讲,不是我主观臆测的,我跑了
1
中第一篇文章中的一个例子:
|
|
上面代码有两个goroutine,一个是main goroutine
,一个是deadloop goroutine
,跑得时候deadloop gouroutine
不会对main goroutine
造成影响,打印一直在持续,作者的文章解释了原因。
- 如何定位 golang 进程 hang 死的 bug这篇文章提到了
gcwaiting
,然而没有解释。
在如何定位 golang 进程 hang 死的 bug有这样一段话:
因为在 for 循环中没有函数调用的话,编译器不会插入调度代码,所以这个执行 for 循环的 goroutine 没有办法被调出,而在循环期间碰到 gc,那么就会卡在 gcwaiting 阶段,并且整个进程永远 hang 死在这个循环上。并不再对外响应。
这个其实就是我们的第一段代码卡死的原因,也是我们第二段代码没有卡死的原因,就是在gc
上!
我们再看一篇文章,golang的垃圾回收(GC)机制,这篇文章很短,但每句话都很重要:
- 设置gcwaiting=1,这个在每一个G任务之前会检查一次这个状态,如是,则会将当前M 休眠;
- 如果这个M里面正在运行一个长时间的G任务,咋办呢,难道会等待这个G任务自己切换吗?这样的话可要等10ms啊,不能等!坚决不能等!
所以会主动发出抢占标记(类似于上一篇),让当前G任务中断,再运行下一个G任务的时候,就会走到第1步
那么如果这时候运行的是没有函数调用的死循环呢,gc也发出了抢占标记,但是如果死循环没有函数调用,就没有地方被标记,无法被抢占,那就只能设置gcwaiting=1
,而M没有休眠,stop the world
卡住了(死锁),gcwaiting
一直是1,整个程序都卡住了!
这里其实已经解释了第一份代码的现象,第二份代码为什么没有hang住相信大家也能猜到了:代码里没有触发gc!我们来手动触发一下:
|
|
会发现打印了3行之后,程序也卡死了,bingo🎉
我们来看看gcwaiting
是不是等于1:
|
|
代码诚不欺我也!