我有以下代码:
func sendRegularHeartbeats(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-time.After(1 * time.Second):
sendHeartbeat()
}
}
}
Run Code Online (Sandbox Code Playgroud)
此功能在专用的go-routine中执行,并每秒发送一次心跳消息.取消上下文时,整个过程应立即停止.
现在考虑以下场景:
ctx, cancel := context.WithCancel(context.Background())
cancel()
go sendRegularHeartbeats(ctx)
Run Code Online (Sandbox Code Playgroud)
这将启动具有封闭上下文的心跳例程.在这种情况下,我不希望传输任何心跳.因此,case应立即输入选择中的第一个块.
但是,似乎case无法保证评估块的顺序,并且代码有时会发送心跳消息,即使上下文已被取消.
实现这种行为的正确方法是什么?
我可以在第二个中添加"isContextclosed" - 检查case,但这看起来更像是一个丑陋的解决方法.
接受的答案有一个错误的建议:
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()
}
}
}
Run Code Online (Sandbox Code Playgroud)
由于以下情况,这无济于事:
另一种但仍然不完美的方法是在使用股票代码事件后防止并发 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()
}
}
}
Run Code Online (Sandbox Code Playgroud)
警告:这个问题的问题在于它允许混淆“足够接近”的事件 - 例如,即使股票代码事件提前到达,完成事件也很快到来以抢占心跳。目前还没有完美的解决方案。
事先注意:
您的示例将按照您的意图运行,就好像上下文在sendRegularHeartbeats()被调用时已被取消一样,case <-ctx.Done()通信将是唯一准备继续并因此被选择的通信.另一个case <-time.After(1 * time.Second)只能在1秒后继续进行,所以一开始就不会选择它.但要在多个案例准备就绪时明确处理优先级,请继续阅读.
与语句的case分支(评估顺序是列出的顺序)不同,在语句的分支中没有保证优先级或任何顺序.switchcaseselect
如果一个或多个通信可以继续,则可以通过统一的伪随机选择来选择可以继续的单个通信.否则,如果存在默认情况,则选择该情况.如果没有默认情况,则"select"语句将阻塞,直到至少一个通信可以继续.
如果可以继续进行更多通信,则随机选择一个.期.
如果要保持优先级,则必须自己(手动)执行此操作.您可以使用多个select语句(后续,而不是嵌套)来执行此操作,在较早的 列表中列出具有较高优先级的语句select,同时确保添加default分支以避免阻塞(如果这些语句尚未准备好继续).您的示例需要2个select语句,首先检查<-ctx.Done()一个是您想要更高优先级的语句.
我还建议在每次迭代中使用单个time.Ticker而不是调用time.After()(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()
}
}
}
Run Code Online (Sandbox Code Playgroud)
如果在sendRegularHeartbeats()调用时已取消上下文,则不会发送心跳,因为您可以在Go Playground上检查/验证它.
如果您将cancel()呼叫延迟2.5秒,那么将发送正好2个心跳:
ctx, cancel := context.WithCancel(context.Background())
go sendRegularHeartbeats(ctx)
time.Sleep(time.Millisecond * 2500)
cancel()
time.Sleep(time.Second * 2)
Run Code Online (Sandbox Code Playgroud)
在Go Playground尝试这个.