由创作原始 Go Gopher 作品的 Renee French 为“ Go 的旅程”创作的插图。
本文基于 Go 1.14。
死锁是当 Goroutine 被阻塞而无法解除阻塞时产生的一种状态。Go 提供了一个死锁检测器,可以帮助开发人员避免陷入这种情况。
检测
让我们从创建这种情况的示例开始:
主 Goroutine 在 channel 上被阻塞,并等待另一个 Goroutine 将数据写入 channel。然而,没有其他的 Goroutine 在运行,它不能被解除阻塞。这种情况将触发死锁错误:
死锁检测器基于对应用程序创建的线程的分析。如果已创建并活动的线程数大于等待工作的线程数,则会出现死锁情况。
这个公式中不包括为监视系统而创建的线程。
在检测到死锁时将创建四个线程:
一个用于主 goroutine,启动程序的那个。
一个叫做
sysmon
,用于监视系统。一个专用于垃圾收集器的 Goroutine 启动的。
在初始化过程中阻塞主 Goroutine 时创建的一个线程。由于此 Goroutine 被锁定在它的线程上,因此 Go 需要创建一个新的 Goroutine 来为其他 Goroutine 提供运行时间。
每次调用死锁检测器时,也可以通过一些调试信息将其可视化:
每当线程空闲时,就会通知检测器。调试的每一行显示空闲线程的递增数量。当空闲线程数等于活动线程数减去系统线程数时,就会发生死锁。在本例中,我们有三个空闲线程和三个活动线程(四个线程减去系统线程)。由于没有活动线程能够解除阻塞空闲线程,因此存在死锁情况。
但是,这种行为有一些限制。实际上,任何自旋的 Goroutine 都会使死锁检测器失效,因为线程将保持活动状态。
限制
现在,通过发送中断信号使 OS 信号停止程序来改进前面的示例:
这是新的输出:
通过键盘发送中断信号后,程序停止了。不再检测到死锁。具有 signal.Notify
的任何活动程序都将运行后台 goroutine,等待输入信号。该 Goroutine 保持活动状态,并且永远不会使活动线程数等于空闲线程数。这是此 Goroutine 的跟踪:
它的大部分时间都花在等待系统调用上。syscall 中的线程不在空闲列表中,因此不会导致死锁。
但是,可以通过调试工具找到它们。
调试
发现这些死锁的最好方法可能是编写单元测试。编写测试确保一次运行较小的代码段。在这种情况下,不应该受到信号处理程序或阻塞系统调用的干扰。然而,即使这样做 ,测试也会挂起,我们肯定会发现有可疑的地方。
如果你想可视化运行程序上的死锁,可以使用 pprof
之类的工具来可视化它。下面是我们修改后的第一个程序,添加了调试功能:
一旦程序运行,我们就可以使用命令 wget http://localhost:6060/debug/pprof/trace?seconds=5
对我们的应用程序进行配置,该命令会生成 5s 的跟踪信息。 这些痕迹告诉我们所有活动:
没有 Goroutine 一直在运行。可以使用以下命令通过 CPU 配置文件进行确认 go tool pprof http://localhost:6060/debug/pprof/profile?seconds=5
。下面是未显示活动的配置文件:
via: https://medium.com/a-journey-with-go/go-how-are-deadlocks-triggered-2305504ac019
作者:Vincent Blanchon 译者:alandtsang 校对:polaris1119
本文由 GCTT 原创翻译,Go语言中文网 首发。也想加入译者行列,为开源做一些自己的贡献么?欢迎加入 GCTT!
翻译工作和译文发表仅用于学习和交流目的,翻译工作遵照 CC-BY-NC-SA 协议规定,如果我们的工作有侵犯到您的权益,请及时联系我们。
欢迎遵照 CC-BY-NC-SA 协议规定 转载,敬请在正文中标注并保留原文/译文链接和作者/译者等信息。
文章仅代表作者的知识和看法,如有不同观点,请楼下排队吐槽
有疑问加站长微信联系(非本文作者))
