如何收听N个频道?(动态选择语句)


116

要开始执行两个goroutine的无限循环,我可以使用以下代码:

收到味精后,它将启动一个新的goroutine并永远继续下去。

c1 := make(chan string)
c2 := make(chan string)

go DoStuff(c1, 5)
go DoStuff(c2, 2)

for ; true;  {
    select {
    case msg1 := <-c1:
        fmt.Println("received ", msg1)
        go DoStuff(c1, 1)
    case msg2 := <-c2:
        fmt.Println("received ", msg2)
        go DoStuff(c2, 9)
    }
}

我现在想对N个goroutine具有相同的行为,但是在这种情况下select语句将如何显示?

这是我开始的代码位,但是我对如何编写select语句感到困惑

numChans := 2

//I keep the channels in this slice, and want to "loop" over them in the select statemnt
var chans = [] chan string{}

for i:=0;i<numChans;i++{
    tmp := make(chan string);
    chans = append(chans, tmp);
    go DoStuff(tmp, i + 1)

//How shall the select statment be coded for this case?  
for ; true;  {
    select {
    case msg1 := <-c1:
        fmt.Println("received ", msg1)
        go DoStuff(c1, 1)
    case msg2 := <-c2:
        fmt.Println("received ", msg2)
        go DoStuff(c2, 9)
    }
}

4
我认为您想要的是通道复用。 golang.org/doc/effective_go.html#chan_of_chan 基本上,您有一个要收听的单个频道,然后有多个子频道进入了主频道。相关的SO问题:stackoverflow.com/questions/10979608/…–
Brenden

Answers:


152

您可以使用reflect包中的Select函数执行此操作:

func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)

Select执行案例列表中描述的选择操作。像Go select语句一样,它阻塞直到至少一种情况可以继续进行,做出统一的伪随机选择,然后执行该情况。它返回所选案例的索引,如果该案例是接收操作,则返回接收到的值和一个布尔值,该布尔值指示该值是否对应于通道上的发送(而不是因为通道关闭而接收到的零值)。

您传递一个SelectCase结构数组,这些结构标识要选择的通道,操作的方向以及在发送操作的情况下要发送的值。

因此,您可以执行以下操作:

cases := make([]reflect.SelectCase, len(chans))
for i, ch := range chans {
    cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
}
chosen, value, ok := reflect.Select(cases)
// ok will be true if the channel has not been closed.
ch := chans[chosen]
msg := value.String()

您可以在此处尝试更加充实的示例:http : //play.golang.org/p/8zwvSk4kjx


4
这样选择的案件数量是否有实际限制?如果您超越它,那么性能会受到严重影响吗?
Maxim Vladimirsky

4
也许这是我的无能,但是当您通过通道发送和接收复杂结构时,我发现很难真正使用这种模式。正如蒂姆·阿克莱尔(Tim Allclair)所说,通过共享的“汇总”渠道对我来说要容易得多。
宝拉·M·阿尔珀

90

您可以通过将每个通道包装在goroutine中来完成此任务,该例程将消息“转发”到共享的“聚合”通道。例如:

agg := make(chan string)
for _, ch := range chans {
  go func(c chan string) {
    for msg := range c {
      agg <- msg
    }
  }(ch)
}

select {
case msg <- agg:
    fmt.Println("received ", msg)
}

如果您需要知道消息来自哪个通道,可以在将消息转发到聚合通道之前将其包装在带有任何额外信息的结构中。

在我的(有限的)测试中,此方法使用了reflect包,其性能大大提高:

$ go test dynamic_select_test.go -test.bench=.
...
BenchmarkReflectSelect         1    5265109013 ns/op
BenchmarkGoSelect             20      81911344 ns/op
ok      command-line-arguments  9.463s

基准代码在这里


2
您的基准代码不正确,您需要在基准内循环b.N。否则结果(b.N在输出中除以,1和2000000000)将完全没有意义。
戴夫C

2
@DaveC谢谢!结论没有改变,但结果更为理智。
Tim Allclair

1
确实,我对您的基准代码进行了快速修改,以获取一些实际数字。此基准可能仍然缺少某些东西/出错了,但更复杂的反射代码所要做的唯一事情是设置速度更快(使用GOMAXPROCS = 1),因为它不需要一堆goroutine。在其他所有情况下,一个简单的goroutine合并通道就会使反射解消失(大约2个数量级)。
Dave C

2
一个重要的缺点(与该reflect.Select方法相比)是,goroutine在合并的每个通道上执行合并缓冲区的最小值至少为一个值。通常这不会是一个问题,但在一些特定的应用,可能是一个大忌:(。
Dave C制作

1
缓冲的合并通道会使问题变得更糟。问题在于,只有反射解决方案才能具有完全无缓冲的语义。我继续并发布了我正在尝试的测试代码,作为一个单独的答案,以(希望)弄清楚我要说的话。
戴夫C

22

为了扩展对先前答案的一些评论并在此处提供更清楚的比较,此处给出了迄今为止给出的两种方法的示例,这些方法在相同的输入,读取的通道片和调用每个值的函数的情况下也需要知道哪个价值的渠道。

两种方法之间存在三个主要区别:

  • 复杂。尽管这可能部分是读者的偏爱,但我发现频道方法更惯用,直截了当且更具可读性。

  • 性能。在我的Xeon amd64系统上,goroutines + channels执行反射解决方案的速度大约为两个数量级(通常,Go中的反射速度通常较慢,应仅在绝对需要时使用)。当然,如果在函数处理结果或将值写入输入通道中存在任何明显的延迟,则性能差异很容易变得无关紧要。

  • 阻塞/缓冲语义。其重要性取决于用例。通常,这无关紧要,或者goroutine合并解决方案中的少量额外缓冲可能有助于提高吞吐量。但是,如果希望具有这样的语义,即只有一个写程序被解除阻塞,并且其值任何其他写程序被解除阻塞之前已得到充分处理,那么这只能通过反射解决方案来实现。

注意,如果不需要发送通道的“ id”或源通道永远不会关闭,则可以简化这两种方法。

Goroutine合并频道:

// Process1 calls `fn` for each value received from any of the `chans`
// channels. The arguments to `fn` are the index of the channel the
// value came from and the string value. Process1 returns once all the
// channels are closed.
func Process1(chans []<-chan string, fn func(int, string)) {
    // Setup
    type item struct {
        int    // index of which channel this came from
        string // the actual string item
    }
    merged := make(chan item)
    var wg sync.WaitGroup
    wg.Add(len(chans))
    for i, c := range chans {
        go func(i int, c <-chan string) {
            // Reads and buffers a single item from `c` before
            // we even know if we can write to `merged`.
            //
            // Go doesn't provide a way to do something like:
            //     merged <- (<-c)
            // atomically, where we delay the read from `c`
            // until we can write to `merged`. The read from
            // `c` will always happen first (blocking as
            // required) and then we block on `merged` (with
            // either the above or the below syntax making
            // no difference).
            for s := range c {
                merged <- item{i, s}
            }
            // If/when this input channel is closed we just stop
            // writing to the merged channel and via the WaitGroup
            // let it be known there is one fewer channel active.
            wg.Done()
        }(i, c)
    }
    // One extra goroutine to watch for all the merging goroutines to
    // be finished and then close the merged channel.
    go func() {
        wg.Wait()
        close(merged)
    }()

    // "select-like" loop
    for i := range merged {
        // Process each value
        fn(i.int, i.string)
    }
}

反射选择:

// Process2 is identical to Process1 except that it uses the reflect
// package to select and read from the input channels which guarantees
// there is only one value "in-flight" (i.e. when `fn` is called only
// a single send on a single channel will have succeeded, the rest will
// be blocked). It is approximately two orders of magnitude slower than
// Process1 (which is still insignificant if their is a significant
// delay between incoming values or if `fn` runs for a significant
// time).
func Process2(chans []<-chan string, fn func(int, string)) {
    // Setup
    cases := make([]reflect.SelectCase, len(chans))
    // `ids` maps the index within cases to the original `chans` index.
    ids := make([]int, len(chans))
    for i, c := range chans {
        cases[i] = reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(c),
        }
        ids[i] = i
    }

    // Select loop
    for len(cases) > 0 {
        // A difference here from the merging goroutines is
        // that `v` is the only value "in-flight" that any of
        // the workers have sent. All other workers are blocked
        // trying to send the single value they have calculated
        // where-as the goroutine version reads/buffers a single
        // extra value from each worker.
        i, v, ok := reflect.Select(cases)
        if !ok {
            // Channel cases[i] has been closed, remove it
            // from our slice of cases and update our ids
            // mapping as well.
            cases = append(cases[:i], cases[i+1:]...)
            ids = append(ids[:i], ids[i+1:]...)
            continue
        }

        // Process each value
        fn(ids[i], v.String())
    }
}

[ Go游乐场上的完整代码。]


1
值得注意的是,goroutines + channels解决方案不能做任何事情select或不能做任何事情reflect.Select。goroutine将继续旋转直到它们消耗了通道中的所有内容,因此没有明确的方法可以Process1提早退出。如果您有多个阅读器,也可能会出现问题,因为goroutines会从每个通道中缓冲一个项目,而不会发生select
James Henstridge

@JamesHenstridge,关于停下的第一句话是不正确的。您将安排停止Process1的方式与停止Process2的方式完全相同;例如,通过添加一个“停止”通道,该通道在goroutine应该停止时关闭。Process1 selectfor循环中需要两个大小写,而不是for range当前使用的简单循环。Process2将需要放入另一个大小写cases并特殊处理该值i
Dave C

这仍然不能解决您正在从停止早期情况下不使用的通道中读取值的问题。
James Henstridge

0

假设有人正在发送事件,为什么这种方法不起作用?

func main() {
    numChans := 2
    var chans = []chan string{}

    for i := 0; i < numChans; i++ {
        tmp := make(chan string)
        chans = append(chans, tmp)
    }

    for true {
        for i, c := range chans {
            select {
            case x = <-c:
                fmt.Printf("received %d \n", i)
                go DoShit(x, i)
            default: continue
            }
        }
    }
}

8
这是一个自旋循环。在等待输入通道具有值时,这会消耗所有可用的CPU。整点select在多个信道上(没有default子句)是它有效地等待,直到至少一个是不准备纺丝。
戴夫C

0

可能更简单的选择:

为什么不只使用一个通道数组,为什么不将一个通道作为参数传递给在单独的goroutine上运行的函数,然后在使用者goroutine中监听该通道?

这样一来,您可以仅在侦听器中的一个通道上进行选择,从而进行简单选择,并且避免创建新的goroutine来聚合来自多个通道的消息?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.