我有以下代码:
func sendRegularHeartbeats(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-time.After(1 * time.Second):
sendHeartbeat()
}
}
}
此功能在专用的go-routine中执行,并每秒发送一次心跳消息。当上下文被取消时,整个过程应立即停止。
现在考虑以下情况:
ctx, cancel := context.WithCancel(context.Background())
cancel()
go sendRegularHeartbeats(ctx)
这将启动具有封闭上下文的心跳例程。在这种情况下,我不希望传输任何心跳。因此,应立即输入select中的第一个case
块。
但是,似乎无法保证评估case
块的顺序,并且代码有时会发送心跳消息,即使上下文已被取消。
实施此类行为的正确方法是什么?
我可以在第二个case
中添加&#34; isContextclosed&#34; -check,但这看起来更像是问题的一个丑陋的解决方法。
答案 0 :(得分:7)
事先注意:
您的示例将按照您的意图运行,就好像在调用sendRegularHeartbeats()
时已取消上下文一样,case <-ctx.Done()
通信将是唯一准备继续并因此被选择的通信。其他case <-time.After(1 * time.Second)
只能在1秒后继续 ,因此一开始就不会选择它。但是,要在多个案例准备就绪时明确处理优先事项,请继续阅读。
与switch
statement的case
分支不同(评估顺序是列出的顺序),case
分支中没有优先级或任何顺序保证{ {3}}
如果一个或多个通信可以继续,可以通过统一的伪随机选择选择可以继续的单个通信。否则,如果存在默认情况,则选择该情况。如果没有默认情况,则“select”语句将阻塞,直到至少有一个通信可以继续。
如果可以继续进行更多通信,则随机选择一个。周期。
如果要保持优先级,则必须自己(手动)执行此操作。您可以使用多个select
语句(后续,而不是嵌套),在早期 select
中列出具有更高优先级的语句,同时确保添加default
}分支,以避免阻止,如果那些还没有准备好继续。您的示例需要2个select
语句,首先检查<-ctx.Done()
,因为这是您想要更高优先级的语句。
我还建议您使用单个Spec: Select statements:而不是在每次迭代中调用time.Ticker
(time.After()
也会使用time.Ticker
,但它不会重复使用只是“扔掉它”并在下次通话中创建一个新的。)
以下是一个示例实现:
func sendRegularHeartbeats(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
default:
}
select {
case <-ctx.Done():
return
case <-ticker.C:
sendHeartbeat()
}
}
}
如果在调用sendRegularHeartbeats()
时上下文已被取消,则不会发送心跳,因为您可以在time.After()
上检查/验证它。
如果您将cancel()
通话延迟2.5秒,则会发送正好2个心跳:
ctx, cancel := context.WithCancel(context.Background())
go sendRegularHeartbeats(ctx)
time.Sleep(time.Millisecond * 2500)
cancel()
time.Sleep(time.Second * 2)
在Go Playground上试试这个。
答案 1 :(得分:3)
如果绝对关键维持操作优先级,您可以:
sendHeartbeat
还是取消它应该退出这样,在其他频道收到的消息(可能 - 你不能保证并发例程的执行顺序)按照他们的顺序进入第三个频道。重新触发,允许你适当地处理它们。
然而,值得注意的是,这可能不是必需的。如果多个案例同时成功,则select
无法保证哪个case
会执行 。这可能是一件罕见的事情;取消和自动收报机都必须在select
处理之前触发。绝大多数时候,只有一个或另一个会在任何给定的循环迭代中触发,因此它的行为与预期完全一致。如果您能够容忍在取消后再发生一次额外心跳的罕见情况,那么您最好保留所拥有的代码,因为它更有效,更易读。
答案 2 :(得分:1)
接受的答案有错误的建议:
func sendRegularHeartbeats(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
//first select
select {
case <-ctx.Done():
return
default:
}
//second select
select {
case <-ctx.Done():
return
case <-ticker.C:
sendHeartbeat()
}
}
}
由于以下情况,这没有帮助:
另一种但仍不完善的方法是在使用了置顶事件后即防止并发Done()事件(“错误选择”)。
func sendRegularHeartbeats(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
//select as usual
select {
case <-ctx.Done():
return
case <-ticker.C:
//give priority to a possible concurrent Done() event non-blocking way
select {
case <-ctx.Done():
return
default:
}
sendHeartbeat()
}
}
}
注意:这一点的问题在于,它允许混淆“足够接近”的事件-例如即使股票报价事件较早到达,“完成”事件也很快出现,足以抢占心跳。到目前为止,还没有完美的解决方案。