【问题标题】:breaking out of a select statement when all channels are closed当所有通道都关闭时,跳出一个 select 语句
【发布时间】:2012-11-19 21:58:18
【问题描述】:

我有两个独立生成数据的 goroutine,每个将数据发送到一个通道。在我的主 goroutine 中,我想在这些输出进入时使用它们中的每一个,但不关心它们进入的顺序。每个通道在耗尽其输出时会自行关闭。虽然 select 语句是像这样独立使用输入的最佳语法,但我还没有看到一种简洁的方法来循环每个通道,直到两个通道都关闭。

for {
    select {
    case p, ok := <-mins:
        if ok {
            fmt.Println("Min:", p) //consume output
        }
    case p, ok := <-maxs:
        if ok {
            fmt.Println("Max:", p) //consume output
        }
    //default: //can't guarantee this won't happen while channels are open
    //    break //ideally I would leave the infinite loop
                //only when both channels are done
    }
}

我能想到的最好的方法是以下(只是草图,可能有编译错误):

for {
    minDone, maxDone := false, false
    select {
    case p, ok := <-mins:
        if ok {
            fmt.Println("Min:", p) //consume output
        } else {
            minDone = true
        }
    case p, ok := <-maxs:
        if ok {
            fmt.Println("Max:", p) //consume output
        } else {
            maxDone = true
        }
    }
    if (minDone && maxDone) {break}
}

但是,如果您使用两个或三个以上的频道,这似乎会变得站不住脚。我知道的唯一另一种方法是在 switch 语句中使用 timout case,它要么小到足以冒提前退出的风险,要么在最终循环中注入过多的停机时间。有没有更好的方法来测试 select 语句中的通道?

【问题讨论】:

    标签: go


    【解决方案1】:

    当我遇到这样的需求时,我采取了以下方法:

    var wg sync.WaitGroup
    wg.Add(2)
    
    go func() {
      defer wg.Done()
      for p := range mins {
        fmt.Println("Min:", p) 
      }
    }()
    
    go func() {
      defer wg.Done()
      for p := range maxs {
        fmt.Println("Max:", p) 
      }
    }()
    
    wg.Wait()
    

    我知道这不是单一的 for select 循环,但在这种情况下,我觉得如果没有“if”条件,这更具可读性。

    【讨论】:

    • 这确实读起来更优雅,但我不相信fmt.Println 在并发下是安全的——至少对于 Stdout 不是。这实际上就是为什么我试图将通道连接到一个开关中,以便实际的程序输出只发生在主 goroutine 上。
    • 也就是说,如果您将fmt.Println 调用替换为发送到新的共享频道output,然后在wg.Wait() 之后关闭output,我认为这将解决安全问题。然后在 main 上循环 range output。这将启用简洁的范围语法!
    【解决方案2】:

    您的示例解决方案效果不佳。一旦其中一个关闭,它总是可以立即进行通信。这意味着你的 goroutine 永远不会屈服,其他通道可能永远不会准备好。您将有效地进入一个无限循环。我在这里贴了一个例子来说明效果:http://play.golang.org/p/rOjdvnji49

    那么,我将如何解决这个问题? nil 通道永远不会为通信做好准备。因此,每次遇到关闭的频道时,您都可以取消该频道,以确保它不再被选中。此处可运行示例:http://play.golang.org/p/8lkV_Hffyj

    for {
        select {
        case x, ok := <-ch:
            fmt.Println("ch1", x, ok)
            if !ok {
                ch = nil
            }
        case x, ok := <-ch2:
            fmt.Println("ch2", x, ok)
            if !ok {
                ch2 = nil
            }
        }
    
        if ch == nil && ch2 == nil {
            break
        }
    }
    

    至于怕它变得笨拙,我认为不会。很少有频道同时进入太多地方。这种情况很少出现,以至于我的第一个建议就是处理它。将 10 个通道与 nil 进行比较的长 if 语句并不是尝试在 select 中处理 10 个通道时最糟糕的部分。

    【讨论】:

    • 我可能会将if 语句包装到本地函数中。它将不那么混乱,函数名称将有助于使正在发生的事情更加明显(类似于这些方面:play.golang.org/p/347KZdI_Gs)。
    • +1 表示非聪明的解决方案。将这些通道设置为 nil 很重要,这样 select 就不会在它们上面浪费时间,但我可能会使用一个单独的变量来计算打开通道的数量。对于 n=2; n>0; { 然后每次将频道设置为 nil 时,n--.
    • 来自语言规范:“由于 nil 通道上的通信永远无法进行,因此只有 nil 通道且没有默认案例的选择会永远阻塞。”为什么上面的内容不会永远阻塞? [编辑:哦,等等,中断总是发生在循环的第一次迭代之前,两者都是 nil]
    • 我会将if 移动到forfor ch != nil || ch2 != nil {。虽然这是一开始的另一个检查,但你永远不会陷入@voutasaurus 提到的无限循环(如果chch2 一开始都是nil,它仍然可能发生)。在我看来它看起来更干净。
    • 它不会永远循环,只是永远阻塞,但结果是相似的。
    【解决方案3】:

    我写了一个包,它提供了一个功能来解决这个问题(以及其他几个):

    https://github.com/eapache/channels

    https://godoc.org/github.com/eapache/channels

    查看Multiplex 函数。它使用反射来缩放到任意数量的输入通道。

    【讨论】:

      【解决方案4】:

      为什么不使用 goroutine?随着您的频道逐渐关闭,整个事情变成了一个简单的范围循环。

      func foo(c chan whatever, prefix s) {
              for v := range c {
                      fmt.Println(prefix, v)
              }
      }
      
      // ...
      
      go foo(mins, "Min:")
      go foo(maxs, "Max:")
      

      【讨论】:

      • 这实际上是我想到的第一个解决方案。如果您希望调用 go foo 的例程知道 foo 何时完成,则需要为此添加同步。 fmt.Println 也不是线程安全的,您偶尔会得到乱码输出。 log.Println 是线程安全的简单替代方案。
      • @Sonia,您也可以在单独的 gorutine 中使用 log.Println 并向其发送消息。 play ground.
      • @Sonia,我创建了package。嘘-哈哈-哈哈。
      【解决方案5】:

      关闭在某些情况下很好,但不是全部。我不会在这里使用它。相反,我只会使用完成的频道:

      for n := 2; n > 0; {
          select {
          case p := <-mins:
              fmt.Println("Min:", p)  //consume output
          case p := <-maxs:
              fmt.Println("Max:", p)  //consume output
          case <-done:
              n--
          }
      }
      

      操场上的完整工作示例:http://play.golang.org/p/Cqd3lg435y

      【讨论】:

      • 不错的解决方案!比斯蒂芬的解决方案恕我直言更惯用。关闭频道很容易挂断电话,因此提醒您不必关闭频道非常有用。
      • 谢谢你,尼克。我想展示一个替代方案,但在睡了之后,我认为我斯蒂芬和 jnml 的解决方案更强大。例如,如果您在我的解决方案中对缓冲最小值和最大值进行了看似简单的更改,那么您将引入与 done 通道的数据竞争,并且当完成值到达时,程序将丢弃碰巧在缓冲区中的任何输出。另外,我并没有严格回答所问的问题,我推测 OP 可以控制生产者代码。有可能他没有,他必须按照他的描述处理一个被关闭的频道。
      • 我认为使用WaitGroup 可能会更清楚
      猜你喜欢
      • 2013-09-11
      • 1970-01-01
      • 2021-01-05
      • 1970-01-01
      • 2019-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-25
      相关资源
      最近更新 更多