英文:
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 (
"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()
}
Client code
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()
}
答案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.Run
和 provider.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 <-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
}
}()
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 <-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() {
<- 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)
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论