为什么signal.Notify要和buffered channel一起使用

479 阅读2分钟

背景:昨晚实现一个服务注册到consul中,测试过程中使用crl+c使服务退出,consul并不会立刻将该服务移除,而是等待一定时间后(可以自行设置)才会将其移除,于是就想使用signal.Notify实现优雅退出功能使consoul立即移除,使用过程出现一个提示: the channel used with signal.Notify should be buffered

以前实现graceful shutdown功能并没有注意这个问题,上网查资料发现存在一定的风险。现在举例说明:

  • 使用unbuffered channel
func main(){
    ......
    go func() {
		err = server.Serve(lis)
		if err != nil && err != http.ErrServerClosed {
            zap.S().Fatalf("failed to serve: %v", err)
		}
	}()
	//接收终止信号
	quit := make(chan os.Signal)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	if err = client.Agent().ServiceDeregister(serviceID); err != nil {
		zap.S().Info("注销失败")
	}
	zap.S().Info("注销成功")
}

执行上述代码,然后按下ctrl + c,正常情况下你会看到注销成功,如果在接收channel前面处理一些复杂的工作呢?我们使用简单的demo测试一下:

package main

func main() {
    quit := make(chan os.Signal)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)

    time.Sleep(6 * time.Second)

    // Block until a signal is received.
    s :=<-quit
    fmt.Println("signal为:", s)
}

执行上述代码,然后按下ctrl + c,你会发现6秒内,怎么按ctrl + c成会都会退出,直到6秒后,整个程序也不会终止,需要再按ctrl + c才会终止,我们期望是,在前6秒內,按下任何一次ctrl + c,6秒要能正常接收到第一次传来的信号,看看源码究竟是什么原因?

  • 形成的原因

    点击signal.Notify打开singal.go 文档,找到 process 函数,可以看到以下代码:

    for c, h := range handlers.m {
    	if h.want(n) {
    		// send but do not block for it
    		select {
    		case c <- sig:
    		default:
                  }
          }
    }
    

    从上面代码可知,如果使用 unbuffered channel,那么在6秒內接收到的任何信号,都会到 default 条件内,所以造成 Channel 不会收到任何值,这就是为什么6秒內的任何动作,在6秒后都完全收不到。这也是出现蓝色提示(the channel used with signal.Notify should be buffered)的原因。为了避免这件发生,所以通常我们会将 signal channel 的缓冲区设置为 1,来避免需要中断程式时,确保主程式可以收到一个signal信号。