go select语句的强制优先级

maj*_*aja 8 go

我有以下代码:

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,但这看起来更像是一个丑陋的解决方法.

Bal*_*ato 9

接受的答案有一个错误的建议:

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)

由于以下情况,这无济于事:

  1. 两个通道都是空的
  2. 首先选择运行
  3. 两个通道同时收到一条消息
  4. 您处于相同的概率游戏中,就好像您在第一次选择中没有做任何事情一样

另一种但仍然不完美的方法是在使用股票代码事件后防止并发 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)

警告:这个问题的问题在于它允许混淆“足够接近”的事件 - 例如,即使股票代码事件提前到达,完成事件也很快到来以抢占心跳。目前还没有完美的解决方案。


icz*_*cza 7

事先注意:

您的示例将按照您的意图运行,就好像上下文在sendRegularHeartbeats()被调用时已被取消一样,case <-ctx.Done()通信将是唯一准备继续并因此被选择的通信.另一个case <-time.After(1 * time.Second)只能在1秒后继续进行,所以一开始就不会选择它.但要在多个案例准备就绪时明确处理优先级,请继续阅读.


语句case分支(评估顺序是列出的顺序)不同,在语句的分支中没有保证优先级或任何顺序.switchcaseselect

Spec:Select语句引用:

如果一个或多个通信可以继续,则可以通过统一的伪随机选择来选择可以继续的单个通信.否则,如果存在默认情况,则选择该情况.如果没有默认情况,则"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尝试这个.