2012-12-02 48 views
30

我有两个独立产生数据的goroutine,每个goroutine将它们发送到一个通道。在我的主要例程中,我希望在输入时使用这些输出中的每一个,但不关心它们的输入顺序。每个通道在耗尽输出时都会关闭。尽管select语句是像这样独立消费输入的最好语法,但我还没有看到一个简洁的方式来循环每个通道,直到两个通道都关闭。当所有通道关闭时突然出现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案例,该案例要么足够小,以避免提前退出,要么将太多停机时间注入最终的循环中。有没有更好的方法来测试select语句中的通道?

回答

58

您的示例解决方案无法正常工作。一旦它们中的一个关闭,它将立即可用于通信。这意味着你的公司永远不会屈服,其他渠道可能永远不会准备好。你会有效地进入一个无限循环。我举了一个例子来说明这里的效果:http://play.golang.org/p/rOjdvnji49

那么,我该如何解决这个问题呢?一个零通道永远不会准备好进行通信。因此,每次遇到一个关闭的频道时,您都可以删除该频道,确保它不会再次被选中。这里可运行的例子: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进行比较,那么很长的一条语句并不是尝试处理选择中的10个通道的最糟糕的部分。

+1

我可能会将'if'语句包装到本地函数中。它不那么混乱,功能名称会帮助更明显地发生什么(沿着这些方向:http://play.golang.org/p/347KZdI_Gs)。 – Chris

+2

+1为非聪明的解决方案。将这些通道设置为零是很重要的,这样select就不会浪费时间,但我可能会使用一个单独的变量来计算打开通道的数量。对于n = 2; N> 0; {然后每次你设置一个频道到零,n--。 – Sonia

+1

来自语言规范:“由于无声通道上的通信永远无法进行,所以只能选择无通道且永远不会有默认情况下的块。” 为什么上面的块永远不会存在? [编辑:哦等一下,休息总是会发生在循环的第一次迭代之前,两者都是零] – voutasaurus

17

在某些情况下关闭很好,但不是全部。我不会在这里使用它。相反,我只想用一个做渠道:

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

+0

不错的解决方案!更多的惯用去比斯蒂芬的解决方案恕我直言。不得不关闭频道很容易挂断,因此提醒您不必非常有用。 –

+3

谢谢你,尼克。我想表现出另一种选择,但在睡觉之后,我认为我的斯蒂芬和杰姆的解决方案更加强大。例如,如果你在我的解决方案中做了缓冲分钟和最大值的看似简单的改变,你可以用完成的通道引入一个数据竞争,当完成值到达时,程序会丢失发生在缓冲区中的任何输出。另外,我并没有严格按照要求回答问题,我在猜测OP可以控制生产者代码。他可能没有,并且他必须按照他所描述的那样处理一个正在关闭的频道。 – Sonia

+0

我认为这可能会更清楚使用[WaitGroup](https://golang.org/pkg/sync/#WaitGroup) – FrontierPsycho

6

为什么不使用够程?随着你的频道关闭,整个事情变成一个简单的范围循环。

func foo(c chan whatever, prefix s) { 
     for v := range c { 
       fmt.Println(prefix, v) 
     } 
} 

// ... 

go foo(mins, "Min:") 
go foo(maxs, "Max:") 
+1

这实际上是我想到的第一个解决方案。如果你想让例程调用foo来知道foo何时完成,那么你需要为此添加同步。另外fmt.Println不是线程安全的,你可能会偶尔出现乱码。 log.Println是线程安全的简单替代方案。 – Sonia

+0

@Sonia,你也可以在单独的gorutine中使用'log.Println'并发送消息给它。 [play ground](http://play.golang.org/p/rURGMxrei4)。 –

+0

@Sonia,我做了[package](https://github.com/logrusorgru/lg)。嘘,哈,哈,哈,哈。 –