Go语言for-select-quit模式停止goroutine的执行逻辑疑问
问题解答
核心结论
你当前的代码实现中,调用close(quit)时如果FnBusy仍在执行,一定会等待该函数完全执行完成后,才会处理quit信号。
原因解释
- Go语言的
select语句仅会在当前所有待监听case都处于未触发状态时,才会阻塞等待信号。当<-ticker.C触发后,代码逻辑就进入FnBusy执行流程,此时整个goroutine正阻塞在FnBusy的time.Sleep调用上,根本没有进入下一轮循环的select监听逻辑,所以就算quit通道已经关闭,也无法被感知,必须等FnBusy执行返回,回到for循环的select语句时,才会读取到quit的关闭信号。 - 你给出的测试输出还暴露了另一个问题:当
FnBusy执行完成回到select时,quit已经关闭(可读)、ticker.C也有堆积的tick事件(可读),Go的select在多个case同时就绪时会伪随机选择一个执行,所以才会出现连续三次触发FnBusy之后才命中quit退出的情况,整个测试跑了3分钟才结束。
优化方案
如果希望关闭quit时可以中断正在执行的FnBusy,可以通过上下文实现,修改后的示例代码如下:
func FnBusy(ctx context.Context) { log.Println("Entering FnBusy") select { case <-ctx.Done(): log.Println("FnBusy interrupted") return case <-time.After(1 * time.Minute): log.Println("Exiting FnBusy") } } func TestGoRoutineStop(t *testing.T) { quit := make(chan interface{}) ctx, cancel := context.WithCancel(context.Background()) wg := sync.WaitGroup{} ticker := time.NewTicker(10 * time.Millisecond) defer ticker.Stop() defer cancel() wg.Add(1) go func() { defer wg.Done() for { select { case <-quit: log.Println("Quit Called") cancel() return case <-ticker.C: FnBusy(ctx) } } }() time.Sleep(150 * time.Millisecond) close(quit) wg.Wait() }
如果希望优先处理quit信号、避免quit就绪后还继续执行FnBusy,可以提前做非阻塞的quit检查:
go func() { defer wg.Done() for { // 优先检查quit信号 select { case <-quit: log.Println("Quit Called") return default: } select { case <-quit: log.Println("Quit Called") return case <-ticker.C: FnBusy() } } }()
内容的提问来源于stack exchange,提问作者Avinash
相关产品推荐
相关产品推荐

