需求分析 (3个原因)
产生这个需求,通常有以下的原因:
- 这个 goroutine 的运行超出了太多预计的时间,以致后续的计算不再有意义
- 这个 goroutine 阻塞在某个 read/write channel 变得没有响应
- 这个 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)
对于第三种情况,程序员能做的就是:
- 尽量使用 Non-blocking IO (正如 go runtime 那样)
- 尽量使用阻塞粒度较小的 sys calls (对外部调用也一样)
- 业务逻辑总是考虑退出机制,编码时避免潜在的死循环
- 在合适的地方插入响应 channel 的代码,保持一定频率的 channel 响应能力
关于 blocking syscall,需要注意的是 Go runtime 会启动新的 OS 线程去调度剩下的 goroutines,如果不能及时从阻塞中恢复并持续有新的 blocking goroutine 的话,OS 线程数量会线性地增长,这是一种非常不理想的情况,极端例子可以看下面的 “why 1000 goroutine generats 1000 os threads?"。
References
- (golang-nuts) End a blocked goroutine from outside
- (StackOverflow) how to stop a goroutine
- (StackOverflow) in golang, does it make sense to write non-blocking code?
- (golang-nuts) “why 1000 goroutine generats 1000 os threads?”
- (Quora) What happens when a goroutine blocks?
- (知乎) golang的goroutine是如何实现的?
(全文完)