等待 context cancel() 完成的好习惯是什么?

问题描述 投票:0回答:1

鉴于我有下面的程序,在通过调用

service.Start()
函数终止之前使方法取消 (cancel()) 完成(即打印
Done
)的好做法是什么?我只有一个 Go 例程,因此使用
WaitGroup
感觉不太好。当我注释掉
time.Sleep
函数中的最后一行 (
main()
) 时,程序似乎提前完成,因为
service.Start()
不打印 Done。如果我取消注释 2 秒睡眠,则会打印Done。我只是想找到实现取消方法的最佳实践,以在不提前终止程序的情况下完成,但在我看来,使用
WaitGroup
time.Sleep()
似乎对于这个特定程序来说是一种不好的做法。我使用 Go 例程和退出通道的原因是因为
service.Start()
将包含需要定期运行直到程序停止的逻辑。

主要:

func main() {
    log.Infoln("starting service..")
    ctx := context.Background()
    srv := service.Service{}

    exitCh := make(chan os.Signal, 1)
    signal.Notify(exitCh, syscall.SIGTERM, // terminate: stopped by `kill -9 PID`
        syscall.SIGINT, // interrupt: stopped by Ctrl + C
    )

    ctxCancel, cancel := context.WithCancel(ctx)
    go run(ctxCancel, srv, exitCh)

    <-exitCh // blocking until receive exit signal
    cancel()
    time.Sleep(time.Second * 2)
}

func run(ctx context.Context, srv service.Service, exitCh chan<- os.Signal) {
    defer func() {
        exitCh <- syscall.SIGTERM // send terminate signal when application stop naturally
    }()
    err := srv.Start(ctx)
    if err != nil {
        log.Warningf("canceling all operations: %s", err)
    }
}

服务:

type Service struct {}

func (s *Service) Start(ctx context.Context) error {
    for i := 0; i < 5; i++ {
        select {
        case <-ctx.Done():
            log.Println("Done")
            return ctx.Err()
        default:
            log.Printf("i value: %d", i)
            time.Sleep(3 * time.Second)
        }
    }
    return nil
}
go goroutine go-context
1个回答
-1
投票

最好的做法是在自己的例程中运行每个服务,其中通过返回确认关闭。函数或方法签名是 Run(而不是 Start),因为上下文参数适用于整个服务持续时间。

使用

main
中的“完成”回调通道启动每个服务:

fooDone := make(chan struct{})
go func() {
    defer close(fooDone)
    err := foo.Run(ctx)
    if !errors.Is(ctx.Canceled) {
        log.Print("service foo terminated: ", err)
        ctxCancel() // shutdown
    }
}()

SIGTERM 不是评论所述的

kill -9
。 SIGKIL 是。 SIGKILL 无法等待几秒钟才能完成,但 SIGINT 和 SIGTERM 可能应该等待。

继续

main
退出处理:

sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGKILL, syscall.SIGTERM)
var sig os.Signal
select {
case <-ctx.Done():
case sig := <-sigs:
    log.Print("got signal ", sig)
    ctxCancel()
}

// trigger any termination beyond Context
bar.Shutdown()

switch (sig) {
case syscall.SIGINT, syscall.SIGTERM:
    // await termination within reasonable limit
    timeout := timer.After(3*time.Second)
    select {
    case <-fooDone:
    case <-timeout:
        log.Print("foo termination timed out")
    }
    // repeat for each service …
}

if (n, ok := sig.(syscall.Signal); ok) {
    os.exit(128 + int(n)) // Unix convention
}
os.exit(255)

退出代码约定当然是可选的,但它允许重新启动逻辑等。我将记录退出代码作为手册的一部分。

© www.soinside.com 2019 - 2024. All rights reserved.