go select语句的强制优先级

时间:2017-09-13 14:35:59

标签: go

我有以下代码:

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

3 个答案:

答案 0 :(得分:7)

事先注意:

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

switch statementcase分支不同(评估顺序是列出的顺序),case分支中没有优先级或任何顺序保证{ {3}}

引自select statement

  

如果一个或多个通信可以继续,可以通过统一的伪随机选择选择可以继续的单个通信。否则,如果存在默认情况,则选择该情况。如果没有默认情况,则“select”语句将阻塞,直到至少有一个通信可以继续。

如果可以继续进行更多通信,则随机选择一个。周期。

如果要保持优先级,则必须自己(手动)执行此操作。您可以使用多个select语句(后续,而不是嵌套),在早期 select中列出具有更高优先级的语句,同时确保添加default }分支,以避免阻止,如果那些还没有准备好继续。您的示例需要2个select语句,首先检查<-ctx.Done(),因为这是您想要更高优先级的语句。

我还建议您使用单个Spec: Select statements:而不是在每次迭代中调用time.Tickertime.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)

如果绝对关键维持操作优先级,您可以:

  • 从单独的goroutine中的每个频道消费
  • 让每个goroutine向共享的第三个频道写一条消息,指明其类型
  • 让第三个goroutine从该频道消费,阅读它收到的消息,以确定它是否是一个勾号,应该是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()
        }
    }
}

由于以下情况,这没有帮助:

  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()
        }
    }
}

注意:这一点的问题在于,它允许混淆“足够接近”的事件-例如即使股票报价事件较早到达,“完成”事件也很快出现,足以抢占心跳。到目前为止,还没有完美的解决方案。