当所有通道都关闭时,会中断select语句

mat*_*ias 49 go

我有两个goroutines独立生成数据,每个数据都发送到一个频道.在我的主要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
    }
}
Run Code Online (Sandbox Code Playgroud)

我能想到的最好的是以下内容(只是草拟,可能有编译错误):

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}
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您使用超过两个或三个频道,这看起来会变得难以维持.我所知道的唯一另一种方法是在switch语句中使用一个timout案例,该案例要么小到足以提前退出,要么在最终循环中注入太多停机时间.有没有更好的方法来测试select语句中的通道?

Ste*_*erg 91

您的示例解决方案不会很好.一旦其中一个关闭,它将始终可用于立即通信.这意味着你的goroutine永远不会屈服,其他渠道可能永远不会准备好.你会有效地进入无限循环.我发布了一个例子来说明这里的效果: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
    }
}
Run Code Online (Sandbox Code Playgroud)

至于害怕它变得笨拙,我认为不会.您很少有频道同时前往太多地方.这很少会出现,我的第一个建议只是处理它.将10个通道与nil进行比较的长if语句并不是尝试处理选择中的10个通道的最差部分.

  • 我将`if`移到`for`:`for ch!= nil || ch2!= nil {`.虽然这是一开始的另外一个检查,但你永远不会处于无限循环中,@ voutasaurus提到(如果`ch`和`ch2`在开始时都是'nil`,它仍然会发生).在我看来它看起来更干净. (9认同)
  • +1非智能解决方案.将这些通道设置为nil很重要,这样select不会浪费时间,但我可能会使用一个单独的变量来计算打开通道的数量.对于n = 2; N> 0; {然后每次将通道设置为nil,n--. (4认同)
  • 我可能会将“ if”语句包装到本地函数中。这样会减少混乱,函数名称将有助于使所发生的事情更加明显(遵循以下几句话:http://play.golang.org/p/347KZdI_Gs)。 (2认同)
  • 从语言规范来看:“由于无法在nil通道上进行通信,因此只有nil通道且没有默认情况下的选择永远不会阻塞。” 为何上述障碍永远不会消失?[编辑:哦,是的,中断总是在循环的第一个迭代都为零的情况下发生] (2认同)

Son*_*nia 24

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

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--
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 谢谢,尼克.我想展示另一种选择,但在睡觉之后,我认为Stephen和jnml的解决方案更加强大.例如,如果您在我的解决方案中进行看似简单的缓冲分钟和最大值更改,则会引入带有完成通道的数据竞争,并且当完成值到达时,程序将丢弃缓冲区中发生的任何输出.另外,我并没有严格回答所提出的问题,我猜测OP可以控制生产者代码.有可能他没有,并且他必须按照他的描述来处理正在关闭的频道. (3认同)

zzz*_*zzz 10

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

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

// ...

go foo(mins, "Min:")
go foo(maxs, "Max:")
Run Code Online (Sandbox Code Playgroud)