首页 > 解决方案 > Go ctx.Done() 永远不会在 select 语句中触发

问题描述

我正在开发的模块有以下代码,但我不确定为什么在provider.Shutdown()调用时从未调用过该函数.Stop()

主要过程确实停止了,但我很困惑为什么这不起作用?

package pluto

import (
    "context"
    "fmt"
    "log"
    "sync"
)

type Client struct {
    name string
    providers []Provider
    cancelCtxFunc context.CancelFunc
}

func NewClient(name string) *Client {
    return &Client{name: name}
}

func (c *Client) Start(blocking bool) {
    log.Println(fmt.Sprintf("Starting the %s service", c.name))

    ctx, cancel := context.WithCancel(context.Background())
    c.cancelCtxFunc = cancel // assign for later use

    var wg sync.WaitGroup

    for _, p := range c.providers {
        wg.Add(1)

        provider := p
        go func() {
            provider.Setup()

            select {
                case <-ctx.Done():
                    // THIS IS NEVER CALLED?!??!
                    provider.Shutdown()
                    return
                default:
                    provider.Run(ctx)
            }
        }()
    }

    if blocking {
        wg.Wait()
    }
}

func (c *Client) RegisterProvider(p Provider) {
    c.providers = append(c.providers, p)
}

func (c *Client) Stop() {
    log.Println("Attempting to stop service")
    c.cancelCtxFunc()
}

客户端代码


package main

import (
    "pluto/pkgs/pluto"
    "time"
)

func main() {
    client := pluto.NewClient("test-client")

    testProvider := pluto.NewTestProvider()
    client.RegisterProvider(testProvider)

    client.Start(false)

    time.Sleep(time.Second * 3)
    client.Stop()
}

标签: gobackend

解决方案


case因为它在上下文被取消之前已经选择了另一个。这是您的代码,注释:

    // Start a new goroutine
    go func() {
        provider.Setup()
        
        // Select the first available case
        select {
            // Is the context cancelled right now?
            case <-ctx.Done():
                // THIS IS NEVER CALLED?!??!
                provider.Shutdown()
                return
            // No? Then call provider.Run()
            default:
                provider.Run(ctx)
                // Run returned, nothing more to do, we're not in a loop, so our goroutine returns
        }
    }()

一旦provider.Run被调用,取消上下文不会在显示的代码中做任何事情。provider.Run虽然也得到了上下文,所以可以自由地处理它认为合适的取消。如果您希望您的例程也看到取消,您可以将其包装在一个循环中:

    go func() {
        provider.Setup()

        for {
        select {
            case <-ctx.Done():
                // THIS IS NEVER CALLED?!??!
                provider.Shutdown()
                return
            default:
                provider.Run(ctx)
        }
        }
    }()

这样一来,一旦provider.Run返回,它会select再次经过,如果上下文已被取消,则将调用该案例。但是,如果上下文没有被取消,它会provider.Run 再次调用,这可能是也可能不是你想要的。

编辑:

更典型的是,您会遇到几种情况之一,具体取决于问题的方式provider.Runprovider.Shutdown工作方式,这在问题中并未明确,因此您可以选择:

Shutdown 必须在上下文被取消时调用,并且Run只能调用一次:

go func() {
    provider.Setup()
    go provider.Run(ctx)
    go func() {
        <- ctx.Done()
        provider.Shutdown()
    }()
}

OrRun已经接收到上下文,已经做了与取消上下文时相同的事情Shutdown,因此在取消上下文时调用Shutdown是完全没有必要的:

go provider.Run(ctx)

推荐阅读