breaking out of a select statement when all channe

2020-05-11 11:07发布

I have two goroutines independently producing data, each sending it to a channel. In my main goroutine, I'd like to consume each of these outputs as they come in, but don't care the order in which they come in. Each channel will close itself when it has exhausted its output. While the select statement is the nicest syntax for consuming inputs independently like this, I haven't seen a concise way for looping over each of until both of the channels have closed.

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

the best I can think to do is the following (just sketched, may have compile errors):

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

But this looks like it would get untenable if you're working with more than two or three channels. The only other method I know of is to use a timout case in the switch statement, which will either be small enough to risk exiting early, or inject too much downtime into the final loop. Is there a better way to test for channels being within a select statement?

标签: go
5条回答
一夜七次
2楼-- · 2020-05-11 11:50

Why not use goroutines? As your channels are getting closed, the whole thing turns into a simple range loop.

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

// ...

go foo(mins, "Min:")
go foo(maxs, "Max:")
查看更多
Rolldiameter
3楼-- · 2020-05-11 11:59

When I came across such a need I took the below approach:

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

I know this is not with single for select loop, but in this case I feel this is more readable without 'if' condition.

查看更多
做个烂人
4楼-- · 2020-05-11 12:01

Close is nice in some situations, but not all. I wouldn't use it here. Instead I would just use a done channel:

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

Complete working example at the playground: http://play.golang.org/p/Cqd3lg435y

查看更多
小情绪 Triste *
5楼-- · 2020-05-11 12:06

I wrote a package which provides a function to solve this problem (among several others):

https://github.com/eapache/channels

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

Check out the Multiplex function. It uses reflection to scale to an arbitrary number of input channels.

查看更多
▲ chillily
6楼-- · 2020-05-11 12:07

Your example solution would not work well. Once one of them closed, it would always be available for communication immediately. This means your goroutine will never yield and other channels may never be ready. You would effectively enter an endless loop. I posted an example to illustrate the effect here: http://play.golang.org/p/rOjdvnji49

So, how would I solve this problem? A nil channel is never ready for communication. So each time you run into a closed channel, you can nil that channel ensuring it is never selected again. Runable example here: 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
    }
}

As for being afraid of it becoming unwieldy, I don't think it will. It is very rare you have channels going to too many places at once. This would come up so rarely that my first suggestion is just to deal with it. A long if statement comparing 10 channels to nil is not the worst part of trying to deal with 10 channels in a select.

查看更多
登录 后发表回答