Search code examples
goselectconcurrencygoroutinechannel

Changing channel from unbuffered to buffered prevents goroutine from running


Here is an exercise using channels and select in a goroutine. If the disconnect channel is changed to a buffered channel the goroutine doesn't run at all.

Why does changing from an unbuffered to a buffered channel prevent running the goroutine?

func SelectDemo(wg *sync.WaitGroup) {

    messageCh := make(chan int, 10)
    disconnectCh := make(chan struct{})
    //  go routine won't run if channel is buffered
    //disconnectCh := make(chan struct{}, 1)

    defer close(messageCh)
    defer close(disconnectCh)
    go func() {
        fmt.Println("  goroutine")
        wg.Add(1)
        for {
            select {
            case v := <-messageCh:
                fmt.Println(v)
            case <-disconnectCh:
                fmt.Println("  disconnectCh")
                //  empty the buffered channel before exiting
                for {
                    select {
                    case v := <-messageCh:
                        fmt.Println(v)
                    default:
                        fmt.Println("  disconnection, return")
                        wg.Done()
                        return
                    }
                }
            }
        }
    }()

    fmt.Println("Sending ints")
    for i := 0; i < 10; i++ {
        messageCh <- i
    }

    fmt.Println("Sending done")
    disconnectCh <- struct{}{}
}

Here's the code to call the function from main. I use the wait group to assure that the goroutine completes before the program exits:

wg := sync.WaitGroup{}
ch09.SelectDemo(&wg)
wg.Wait()

Solution

  • That code logic has many flaws - some of them are:
    1- Since the messageCh is buffered, this code is not blocking:

        for i := 0; i < 10; i++ {
            messageCh <- i
        }
    

    so the next code is in the fast path to run:

    disconnectCh <- struct{}{}
    

    if you make the disconnectCh buffered, this line runs without blocking too, and the SelectDemo function may exit befor running the wg.Add(1).

    So: You must put:

    wg.Add(1)
    

    before

    go func() {
    

    2- Even with wg.Add(1) before go func() { code -
    you have:

        defer close(messageCh)
        defer close(disconnectCh)
    

    which will close both channels at SelectDemo function return And this select is a random selection since both channels are ready:

    fmt.Println("  goroutine")
            for {
                select {
                case v := <-messageCh:
                    fmt.Println(v)
                case <-disconnectCh:
    

    and it is highly likely that the second select:

                    for {
                        select {
                        case v := <-messageCh:
                            fmt.Println(v)
                        default:
                            fmt.Println("  disconnection, return")
                            wg.Done()
                            return
                        }
                    }
    

    will run forever since the messageCh is closed, returning 0 forever after channel data read:

    case v := <-messageCh:
        fmt.Println(v)