Go语言中的`ctx.Done()`在`select`语句中永远不会触发。

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

Go ctx.Done() never firing in select statement

问题

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

主进程确实停止了,但我对为什么不起作用感到困惑。

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()
}
英文:

I have the following code for a module I'm developing and I'm not sure why the provider.Shutdown() function is never called when I called .Stop()

The main process does stop but I'm confused why this doesn't work?

package pluto

import (
    &quot;context&quot;
    &quot;fmt&quot;
    &quot;log&quot;
    &quot;sync&quot;
)

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

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

func (c *Client) Start(blocking bool) {
    log.Println(fmt.Sprintf(&quot;Starting the %s service&quot;, 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 &lt;-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(&quot;Attempting to stop service&quot;)
    c.cancelCtxFunc()
}

Client code


package main

import (
	&quot;pluto/pkgs/pluto&quot;
	&quot;time&quot;
)

func main() {
	client := pluto.NewClient(&quot;test-client&quot;)

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

	client.Start(false)

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

答案1

得分: 1

因为在上下文被取消之前,已经选择了其他的case。以下是你的代码注释:

// 启动一个新的 goroutine
go func() {
provider.Setup()

// 选择第一个可用的 case
select {
// 上下文是否已经取消?
case <-ctx.Done():
// 这个永远不会被调用?!?!
provider.Shutdown()
return
// 不是?那就调用 provider.Run()
default:
provider.Run(ctx)
// Run 返回后,没有其他事情要做,我们不在循环中,所以 goroutine 返回
}

}()

一旦调用了 provider.Run,取消上下文在所示的代码中不会起作用。不过,provider.Run 也会接收到上下文,因此它可以根据自己的需要处理取消操作。如果你希望你的例程也能看到取消操作,你可以将其包装在一个循环中:

go func() {
provider.Setup()

for {
select {
case <-ctx.Done():
// 这个永远不会被调用?!?!
provider.Shutdown()
return
default:
provider.Run(ctx)
}
}

}()

这样,一旦 provider.Run 返回,它将再次进入 select,如果上下文已被取消,那么该 case 将被调用。然而,如果上下文没有被取消,它将再次调用 provider.Run,这可能是你想要的,也可能不是。

编辑:

根据 provider.Runprovider.Shutdown 的工作方式,通常会有几种情况,这在问题中没有明确说明,所以这里给出几个选项:

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

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

或者,Run 已经接收到上下文,并且在上下文被取消时已经执行了与 Shutdown 相同的操作,因此在上下文被取消时调用 Shutdown 是完全不必要的:

go provider.Run(ctx)

英文:

Because it's already chosen the other case before the context is cancelled. Here is your code, annotated:

    // Start a new goroutine
go func() {
provider.Setup()
// Select the first available case
select {
// Is the context cancelled right now?
case &lt;-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&#39;re not in a loop, so our goroutine returns
}
}()

Once provider.Run is called, cancelling the context isn't going to do anything in the code shown. provider.Run also gets the context though, so it is free to handle cancellation as it sees fit. If you want your routine to also see cancellation, you could wrap this in a loop:

    go func() {
provider.Setup()
for {
select {
case &lt;-ctx.Done():
// THIS IS NEVER CALLED?!??!
provider.Shutdown()
return
default:
provider.Run(ctx)
}
}
}()

This way, once provider.Run returns, it will go through the select again, and if the context has been cancelled, that case will be called. However, if the context hasn't been cancelled, it'll call provider.Run again, which may or may not be what you want.

EDIT:

More typically, you'd have one of a couple scenarios, depending on how provider.Run and provider.Shutdown work, which hasn't been made clear in the question, so here are your options:

Shutdown must be called when the context is cancelled, and Run must only be called once:

go func() {
provider.Setup()
go provider.Run(ctx)
go func() {
&lt;- ctx.Done()
provider.Shutdown()
}()
}

Or Run, which already receives the context, already does the same thing as Shutdown when the context is cancelled, and therefore calling Shutdown when the context is cancelled is wholly unnecessary:

go provider.Run(ctx)

huangapple
  • 本文由 发表于 2021年6月8日 03:29:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/67877651.html
匿名

发表评论

匿名网友

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

确定