协作/非协作式抢占
Go 最近有一个比较有意思的 proposal,提出要将协作式的抢占调度修改为非协作式的抢占调度,并使 Go 的每一条指令理论上都可以被抢占。起因是因为之前有类似这样的问题。当然,社区里遇到问题的人很多,如果你感兴趣,还可以看看这些 github 上的 issue:#543, #12553, #13546, #14561, #15442, #17174, #20793, #21053。
之前的协作式抢占是怎么一回事呢,我翻译的这篇 里其实已经有提及,该文中提到的函数中的 prologue 和 epilogue 即是可以执行调度的一个典型时机(但不是全部,此外还包括有 syscall,channel 操作时等调度点)。可以调度,意味着在这个节点上可以抢占 goroutine,将其挂起,然后就可以进行 GC 了,调度点可以认为是 GC 的 safepoint。上述例子之所以会 hang 死,就是因为正在执行死循环的 g 可能永远都到不了 safepoint,所以调度器也拿它没办法。只能一直等待下去。这样用户侧看来的表现就可能是整个进程直接 hang 死了。
在提出修改调度之前,Go 的开发组还尝试了其它的一些方案,比如想办法在 for 循环的 backedge 上插入调度指令,可以参见这里。这个改动影响了一些数学函数的性能,比如 geomean 几何平均函数,性能下降了 7.8 %。想想的话也是必然的结果,毕竟你在循环里插入了额外的检查指令。
看计划,这个变动会在 1.12 上完全实现。对于用户来讲,由于这些修改没有 API 变动,所以并没有什么可感知的变化。唯一的变化大概是就算有死循环的 goroutine,也不会在 GC 的时候卡死了。