使用带有取消的上下文,Go 例程不会终止
Posted
技术标签:
【中文标题】使用带有取消的上下文,Go 例程不会终止【英文标题】:Using context with cancel, Go routine doesn't terminate 【发布时间】:2021-10-31 23:33:57 【问题描述】:我是 Go 和 Go 并发的新手。一旦找到具有给定 ID 的成员,我正在尝试使用 Go 上下文取消一组 Go 例程。
一个组存储一个客户列表,每个客户都有一个成员列表。我想并行搜索所有客户及其所有成员,以找到具有给定 ID 的成员。一旦找到此成员,我想取消所有其他 Go 例程并返回发现的成员。
我尝试了以下实现,使用 context.WithCancel 和 WaitGroup。
但是这不起作用,并且无限期挂起,永远不会超过 waitGroup.Wait() 行,但我不确定为什么。
func (group *Group) MemberWithID(ID string) (*models.Member, error)
found := make(chan *models.Member)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var waitGroup sync.WaitGroup
for _, client := range group.Clients
waitGroup.Add(1)
go func(clientToQuery Client)
defer waitGroup.Done()
select
case <-ctx.Done():
return
default:
member, _ := client.ClientMemberWithID(ID)
if member != nil
found <- member
cancel()
return
(client)
waitGroup.Wait()
if len(found) > 0
return <-found, nil
return nil, fmt.Errorf("no member found with given id")
【问题讨论】:
【参考方案1】:found
是一个无缓冲通道,因此在它上面发送会阻塞,直到有人准备好接收它为止。
您的main()
函数将是从它接收的函数,但仅在waitGroup.Wait()
返回之后。但这将阻塞,直到所有启动的 goroutine 调用 waitGroup.Done()
。但这在他们返回之前不会发生,直到他们可以发送 found
才会发生。这是一个僵局。
如果您将found
更改为缓冲,即使main()
尚未准备好从其接收(与缓冲区一样多的值),也将允许在其上发送值。
但是您应该在waitGroup.Wait()
返回之前收到来自found
的信息。
另一种解决方案是为found
使用1 的缓冲区,并在found
上使用非阻塞发送。这样,第一个(最快的)goroutine 将能够发送结果,而其余的(假设我们使用的是非阻塞发送)将简单地跳过发送。
还要注意,调用cancel()
的应该是main()
,而不是每个单独启动的goroutine。
【讨论】:
哦,我明白了,我怎么知道在找到成员时从 main 调用取消? 在这种情况下,我什至不会使用Context
:你并没有真正使用它。 client.ClientMemberWithID
不使用它,所以在这里它相当没用,只会使事情复杂化。如果您在执行数据库查询时传递/使用它会很有用。【参考方案2】:
对于这种用例,我认为sync.Once
可能比频道更适合。当你找到第一个非 nil 成员时,你想做两件不同的事情:
-
记录您找到的成员。
取消剩余的 goroutine。
缓冲通道可以轻松完成 (1),但会使 (2) 变得更复杂一些。但是sync.Once
非常适合在第一次发生有趣的事情时做两件不同的事情!
我还建议汇总重要错误,以便在数据库连接失败或发生其他重要错误时报告比no member found
更有用的内容。你也可以使用sync.Once
!
综合起来,我希望看到这样的东西 (https://play.golang.org/p/QZXUUnbxOv5):
func (group *Group) MemberWithID(ctx context.Context, id string) (*Member, error)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var (
wg sync.WaitGroup
member *Member
foundOnce sync.Once
firstNontrivialErr error
errOnce sync.Once
)
for _, client := range group.Clients
wg.Add(1)
client := client // https://golang.org/doc/faq#closures_and_goroutines
go func()
defer wg.Done()
m, err := client.ClientMemberWithID(ctx, id)
if m != nil
foundOnce.Do(func()
member = m
cancel()
)
else if nf := (*MemberNotFoundError)(nil); !errors.As(err, &nf)
errOnce.Do(func()
firstNontrivialErr = err
)
()
wg.Wait()
if member == nil
if firstNontrivialErr != nil
return nil, firstNontrivialErr
return nil, &MemberNotFoundErrorID: id
return member, nil
【讨论】:
使用缓冲通道而不是sync.Once
(play.golang.org/p/blsYV_eMgN_X) 的等效方法也可以,但对我来说似乎有点冗长。
非常感谢这个解决方案,sync.Once 似乎真的很有用,我以前没听说过以上是关于使用带有取消的上下文,Go 例程不会终止的主要内容,如果未能解决你的问题,请参考以下文章