goroutine没有看到上下文取消吗?

huangapple go评论153阅读模式
英文:

goroutine not seeing context cancel?

问题

我有两个 goroutine 同时运行。

在某个时刻,我希望程序能够优雅地退出,所以我使用 cancel() 函数通知我的 goroutine 需要停止,但只有其中一个接收到了消息。

这是我的主函数(简化版):

  1. ctx := context.Background()
  2. ctx, cancel := context.WithCancel(ctx)
  3. done := make(chan os.Signal, 1)
  4. signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
  5. wg := &sync.WaitGroup{}
  6. wg.Add(2)
  7. go func() {
  8. err := eng.Watcher(ctx, wg)
  9. if err != nil {
  10. cancel()
  11. }
  12. }()
  13. go func() {
  14. err := eng.Suspender(ctx, wg)
  15. if err != nil {
  16. cancel()
  17. }
  18. }()
  19. <-done // 等待 SIGINT / SIGTERM
  20. log.Print("接收到关闭信号")
  21. cancel()
  22. wg.Wait()
  23. log.Print("控制器正常退出")

Suspender goroutine 成功退出(以下是代码):

  1. package main
  2. import (
  3. "context"
  4. "sync"
  5. "time"
  6. log "github.com/sirupsen/logrus"
  7. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  8. "k8s.io/client-go/util/retry"
  9. )
  10. func (eng *Engine) Suspender(ctx context.Context, wg *sync.WaitGroup) error {
  11. contextLogger := eng.logger.WithFields(log.Fields{
  12. "go-routine": "Suspender",
  13. })
  14. contextLogger.Info("启动 Suspender goroutine")
  15. now := time.Now().In(eng.loc)
  16. for {
  17. select {
  18. case n := <-eng.Wl:
  19. // 做一些事情
  20. case <-ctx.Done():
  21. // 上下文结束,停止处理结果
  22. contextLogger.Infof("goroutine Suspender 被上下文取消")
  23. return nil
  24. }
  25. }
  26. }

以下是没有接收到上下文取消的函数:

  1. package main
  2. import (
  3. "context"
  4. "sync"
  5. "time"
  6. log "github.com/sirupsen/logrus"
  7. )
  8. func (eng *Engine) Watcher(ctx context.Context, wg *sync.WaitGroup) error {
  9. contextLogger := eng.logger.WithFields(log.Fields{
  10. "go-routine": "Watcher",
  11. "uptime-schedule": eng.upTimeSchedule,
  12. })
  13. contextLogger.Info("启动 Watcher goroutine")
  14. ticker := time.NewTicker(time.Second * 30)
  15. for {
  16. select {
  17. case <-ctx.Done():
  18. contextLogger.Infof("goroutine watcher 被上下文取消")
  19. log.Printf("toto")
  20. return nil
  21. case <-ticker.C:
  22. // 做一些事情
  23. }
  24. }
  25. }

请问我能帮到你什么?谢谢 goroutine没有看到上下文取消吗?

英文:

I have two goroutines running at the same time.

At some point, I want my program to exit gracefully so I use the cancel() func to notify my goroutines that they need to be stopped, but only one of the two receive the message.

here is my main (simplified):

  1. ctx := context.Background()
  2. ctx, cancel := context.WithCancel(ctx)
  3. done := make(chan os.Signal, 1)
  4. signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
  5. wg := &amp;sync.WaitGroup{}
  6. wg.Add(2)
  7. go func() {
  8. err := eng.Watcher(ctx, wg)
  9. if err != nil {
  10. cancel()
  11. }
  12. }()
  13. go func() {
  14. err := eng.Suspender(ctx, wg)
  15. if err != nil {
  16. cancel()
  17. }
  18. }()
  19. &lt;-done // wait for SIGINT / SIGTERM
  20. log.Print(&quot;receive shutdown&quot;)
  21. cancel()
  22. wg.Wait()
  23. log.Print(&quot;controller exited properly&quot;)

The Suspender goroutine exist successfully (here is the code):

  1. package main
  2. import (
  3. &quot;context&quot;
  4. &quot;sync&quot;
  5. &quot;time&quot;
  6. log &quot;github.com/sirupsen/logrus&quot;
  7. metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;
  8. &quot;k8s.io/client-go/util/retry&quot;
  9. )
  10. func (eng *Engine) Suspender(ctx context.Context, wg *sync.WaitGroup) error {
  11. contextLogger := eng.logger.WithFields(log.Fields{
  12. &quot;go-routine&quot;: &quot;Suspender&quot;,
  13. })
  14. contextLogger.Info(&quot;starting Suspender goroutine&quot;)
  15. now := time.Now().In(eng.loc)
  16. for {
  17. select {
  18. case n := &lt;-eng.Wl:
  19. //dostuff
  20. case &lt;-ctx.Done():
  21. // The context is over, stop processing results
  22. contextLogger.Infof(&quot;goroutine Suspender canceled by context&quot;)
  23. return nil
  24. }
  25. }
  26. }

and here is the func that is not receiving the context cancellation:

  1. package main
  2. import (
  3. &quot;context&quot;
  4. &quot;sync&quot;
  5. &quot;time&quot;
  6. log &quot;github.com/sirupsen/logrus&quot;
  7. )
  8. func (eng *Engine) Watcher(ctx context.Context, wg *sync.WaitGroup) error {
  9. contextLogger := eng.logger.WithFields(log.Fields{
  10. &quot;go-routine&quot;: &quot;Watcher&quot;,
  11. &quot;uptime-schedule&quot;: eng.upTimeSchedule,
  12. })
  13. contextLogger.Info(&quot;starting Watcher goroutine&quot;)
  14. ticker := time.NewTicker(time.Second * 30)
  15. for {
  16. select {
  17. case &lt;-ctx.Done():
  18. contextLogger.Infof(&quot;goroutine watcher canceled by context&quot;)
  19. log.Printf(&quot;toto&quot;)
  20. return nil
  21. case &lt;-ticker.C:
  22. //dostuff
  23. }
  24. }
  25. }
  26. }

Can you please help me ?

Thanks goroutine没有看到上下文取消吗?

答案1

得分: 2

你尝试过使用errgroup吗?它内置了上下文取消功能:

  1. ctx := context.Background()
  2. ctx, cancel := context.WithCancel(ctx)
  3. defer cancel()
  4. done := make(chan os.Signal, 1)
  5. signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
  6. // "golang.org/x/sync/errgroup"
  7. wg, ctx := errgroup.WithContext(ctx)
  8. wg.Go(func() error {
  9. return eng.Watcher(ctx, wg)
  10. })
  11. wg.Go(func() error {
  12. return eng.Suspender(ctx, wg)
  13. })
  14. wg.Go(func() error {
  15. defer cancel()
  16. <-done
  17. return nil
  18. })
  19. err := wg.Wait()
  20. if err != nil {
  21. log.Print(err)
  22. }
  23. log.Print("receive shutdown")
  24. log.Print("controller exited properly")
英文:

Did you try it with an errgroup? It has context cancellation baked in:

  1. ctx := context.Background()
  2. ctx, cancel := context.WithCancel(ctx)
  3. defer cancel()
  4. done := make(chan os.Signal, 1)
  5. signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
  6. // &quot;golang.org/x/sync/errgroup&quot;
  7. wg, ctx := errgroup.WithContext(ctx)
  8. wg.Go(func() error {
  9. return eng.Watcher(ctx, wg)
  10. })
  11. wg.Go(func() error {
  12. return eng.Suspender(ctx, wg)
  13. })
  14. wg.Go(func() error {
  15. defer cancel()
  16. &lt;-done
  17. return nil
  18. })
  19. err := wg.Wait()
  20. if err != nil {
  21. log.Print(err)
  22. }
  23. log.Print(&quot;receive shutdown&quot;)
  24. log.Print(&quot;controller exited properly&quot;)

答案2

得分: 0

表面上看,代码看起来不错。我唯一能想到的是在"dostuff"中可能很繁忙。在调试器中逐步执行与时间相关的代码可能会很棘手,所以尝试添加一些日志记录:

  1. case <-ticker.C:
  2. log.Println("doing stuff")
  3. //dostuff
  4. log.Println("done stuff")

(我还假设你在go协程中的某个地方调用了wg.Done(),尽管如果缺少这些调用,那也不会导致你所描述的问题。)

英文:

On the surface the code looks good. The only thing I can think is that it's busy in "dostuff". It can be tricky to step through timing related code in the debugger so try adding some logging:

  1. case &lt;-ticker.C:
  2. log.Println(&quot;doing stuff&quot;)
  3. //dostuff
  4. log.Println(&quot;done stuff&quot;)

(I also assume you are calling wg.Done() in your go-routines somewhere though if they are missing that would not be the cause of the problem you describe.)

答案3

得分: 0

SuspenderWatcher中的代码没有通过Done()方法调用来减少waitgroup计数器 - 这是无限执行的原因。

老实说,忘记这样的小事情是相当正常的。这就是为什么作为Go的标准通用实践,建议使用defer并在函数/方法的最开始处理那些关键的事情。

更新后的实现可能如下所示:

  1. func (eng *Engine) Suspender(ctx context.Context, wg *sync.WaitGroup) error {
  2. defer wg.Done()
  3. // ------------------------------------
  4. func (eng *Engine) Watcher(ctx context.Context, wg *sync.WaitGroup) error {
  5. defer wg.Done()
  6. contextLogger := eng.logger.WithFields(log.Fields{

另外,另一个建议是,查看主例程时,建议始终将context按值传递给任何被调用的go例程或方法调用(lambda)。
这种方法可以帮助开发人员避免很难注意到的与程序相关的错误。

  1. go func(ctx context.Context) {
  2. err := eng.Watcher(ctx, wg)
  3. if err != nil {
  4. cancel()
  5. }
  6. }(ctx)

编辑-1:(确切的解决方案)

尝试按我之前提到的方式使用值传递来传递上下文。否则,两个go例程将使用相同的上下文(因为您正在引用它),并且只会触发一个ctx.Done()
通过将ctx作为值传递,Go中创建了两个独立的子上下文。在使用cancel()关闭父上下文时,两个子上下文都会独立触发ctx.Done()

英文:

The code in Suspender and in Watcher doesn't decrement the waitgroup counter through the Done() method call - the reason behind the infinite execution.

And to be honest it's quite normal to forget such small things. That's why as a standard general practice in Go, it is suggested to use defer and handle things that are critical (and should be handled inside the function/method ) at the very beginning.

The updated implementation might look like

  1. func (eng *Engine) Suspender(ctx context.Context, wg *sync.WaitGroup) error {
  2. defer wg.Done()
  3. // ------------------------------------
  4. func (eng *Engine) Watcher(ctx context.Context, wg *sync.WaitGroup) error {
  5. defer wg.Done()
  6. contextLogger := eng.logger.WithFields(log.Fields{

Also, another suggestion, looking at the main routine, it is always suggested to pass context by value to any go-routine or method calls (lambda) that are being invoked.
This approach saves developers from a lot of program-related bugs that can't be noticed very easily.

  1. go func(ctx context.Context) {
  2. err := eng.Watcher(ctx, wg)
  3. if err != nil {
  4. cancel()
  5. }
  6. }(ctx)

Edit-1: (the exact solution)

Try passing the context using the value in the go routines as I mentioned earlier. Otherwise, both of the go routine will use a single context (because you are referencing it) and only one ctx.Done() will be fired.
By passing ctx as a value 2 separate child contexts are created in Go. And while closing parent with cancel() - both children independently fires ctx.Done().

huangapple
  • 本文由 发表于 2022年6月12日 00:40:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/72586157.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定