Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does select work when multiple channels are involved?

I found when using select on multiple non buffered channels like

select {
case <- chana:
case <- chanb:
}

Even when both channels have data, but when processing this select, the call that falls in case chana and case chanb is not balanced.

package main

import (
    "fmt"
    _ "net/http/pprof"
    "sync"
    "time"
)

func main() {
    chana := make(chan int)
    chanb := make(chan int)

    go func() {
        for i := 0; i < 1000; i++ {
            chana <- 100 * i
        }
    }()

    go func() {
        for i := 0; i < 1000; i++ {
            chanb <- i
        }
    }()

    time.Sleep(time.Microsecond * 300)

    acount := 0
    bcount := 0
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
        for {
            select {
            case <-chana:
                acount++
            case <-chanb:
                bcount++
            }
            if acount == 1000 || bcount == 1000 {
                fmt.Println("finish one acount, bcount", acount, bcount)
                break
            }
        }
        wg.Done()
    }()

    wg.Wait()
}

Run this demo, when one of the chana,chanb finished read/write, the other may remain 999-1 left.

Is there any method to ensure the balance?

found related topic
golang-channels-select-statement

like image 747
Terry Pang Avatar asked Dec 05 '17 03:12

Terry Pang


People also ask

How does select work in Golang?

In Go language, the select statement is just like switch statement, but in the select statement, case statement refers to communication, i.e. sent or receive operation on the channel. Important points: Select statement waits until the communication(send or receive operation) is prepared for some cases to begin.

What is the difference between the switch statement and the select statement in go?

A select will choose multiple valid options at random, while a switch will go in sequence (and would require a fallthrough to match multiple.)

How do I close a channel in Golang?

We can close a channel in Golang with the help of the close() function. Once a channel is closed, we can't send data to it, though we can still read data from it. A closed channel denotes a case where we want to show that the work has been done on this channel, and there's no need for it to be open.


1 Answers

The Go select statement is not biased toward any (ready) cases. Quoting from the spec:

If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.

If multiple communications can proceed, one is selected randomly. This is not a perfect random distribution, and the spec does not guarantee that, but it's random.

What you experience is the result of the Go Playground having GOMAXPROCS=1 (which you can verify here) and the goroutine scheduler not being preemptive. What this means is that by default goroutines are not executed parallel. A goroutine is put in park if a blocking operation is encountered (e.g. reading from the network, or attempting to receive from or send on a channel that is blocking), and another one ready to run continues.

And since there is no blocking operation in your code, goroutines may not be put in park and it may be only one of your "producer" goroutines will run, and the other may not get scheduled (ever).

Running your code on my local computer where GOMAXPROCS=4, I have very "realistic" results. Running it a few times, the output:

finish one acount, bcount 1000 901
finish one acount, bcount 1000 335
finish one acount, bcount 1000 872
finish one acount, bcount 427 1000

If you need to prioritize a single case, check out this answer: Force priority of go select statement

The default behavior of select does not guarantee equal priority, but on average it will be close to it. If you need guaranteed equal priority, then you should not use select, but you could do a sequence of 2 non-blocking receive from the 2 channels, which could look something like this:

for {
    select {
    case <-chana:
        acount++
    default:
    }
    select {
    case <-chanb:
        bcount++
    default:
    }
    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

The above 2 non-blocking receive will drain the 2 channels at equal speed (with equal priority) if both supply values, and if one does not, then the other is constantly received from without getting delayed or blocked.

One thing to note about this is that if none of the channels provide any values to receive, this will be basically a "busy" loop and hence consume computational power. To avoid this, we may detect that none of the channels were ready, and then use a select statement with both of the receives, which then will block until one of them is ready to receive from, not wasting any CPU resources:

for {
    received := 0
    select {
    case <-chana:
        acount++
        received++
    default:
    }
    select {
    case <-chanb:
        bcount++
        received++
    default:
    }

    if received == 0 {
        select {
        case <-chana:
            acount++
        case <-chanb:
            bcount++
        }
    }

    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

For more details about goroutine scheduling, see these questions:

Number of threads used by Go runtime

Goroutines 8kb and windows OS thread 1 mb

Why does it not create many threads when many goroutines are blocked in writing file in golang?

like image 95
icza Avatar answered Oct 17 '22 12:10

icza