breaking out of a select statement when all channels are closed

Go

Go Problem Overview


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 Solutions


Solution 1 - Go

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.

Solution 2 - Go

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

Solution 3 - Go

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:")

Solution 4 - Go

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.

Solution 5 - Go

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmatthiasView Question on Stackoverflow
Solution 1 - GoStephen WeinbergView Answer on Stackoverflow
Solution 2 - GoSoniaView Answer on Stackoverflow
Solution 3 - GozzzzView Answer on Stackoverflow
Solution 4 - GoMadhan GaneshView Answer on Stackoverflow
Solution 5 - GoEvanView Answer on Stackoverflow