(Go) 从外部结束一个 goroutine

go

需求分析 (3个原因)

产生这个需求,通常有以下的原因:

  1. 这个 goroutine 的运行超出了太多预计的时间,以致后续的计算不再有意义
  2. 这个 goroutine 阻塞在某个 read/write channel 变得没有响应
  3. 这个 goroutine 阻塞在某个系统调用,外部调用或业务逻辑的死循环

这种时候很自然地就会产生“主动外部 kill goroutine”的需求 (正如手动结束掉一个无响应的进程那样)。

然而 goroutine 被设计为不可以从外部无条件地结束掉,只能通过 channel 来与它通信。也就是说,每一个 goroutine 都需要承担自己退出的责任。(A goroutine cannot be programmatically killed. It can only commit a cooperative suicide.)

以下我们分可响应 (1 & 2) 和不可响应 (3) 两种情况分开讨论

处理仍可响应 channel 的 goroutine (1 & 2)

最直接的方法是关闭与这个 goroutine 通信的 channel close(ch)。如果这个 goroutine 此时阻塞在 read 上,那么阻塞会失效,并在第二个返回值中返回 false (此时可以检测并退出);如果阻塞在 write 上,那么会 panic,这时合理的做法是在 goroutine 的顶层 recover 并退出。

更健壮的设计一般会把 data channel (用于传递业务逻辑的数据) 和 signal channel (用于管理 goroutine 的状态) 分开。不会让 goroutine 直接读写 data channel,而是通过 select-default 或 select-timeout 来避免完全阻塞,同时周期性地在 signal channel 检查是否有结束的请求。

以上的方法可以处理前两种情况。

处理无法响应 channel 的 goroutine (3)

对于第三种情况,程序员能做的就是:

  1. 尽量使用 Non-blocking IO (正如 go runtime 那样)
  2. 尽量使用阻塞粒度较小的 sys calls (对外部调用也一样)
  3. 业务逻辑总是考虑退出机制,编码时避免潜在的死循环
  4. 在合适的地方插入响应 channel 的代码,保持一定频率的 channel 响应能力

关于 blocking syscall,需要注意的是 Go runtime 会启动新的 OS 线程去调度剩下的 goroutines,如果不能及时从阻塞中恢复并持续有新的 blocking goroutine 的话,OS 线程数量会线性地增长,这是一种非常不理想的情况,极端例子可以看下面的 "why 1000 goroutine generats 1000 os threads?"。

References

[完]
Gu Lu
[2016-02-02]

知识共享许可协议
本作品由Gu Lu创作,采用知识共享Attribution-NonCommercial-NoDerivatives 4.0 国际许可协议进行许可。