Search code examples
gogoroutine

Goroutine deadlock


the following goroutine code returns deadlock...

Please help me to identify the issue package main

import "fmt"

func main() {
    // create & consume channel
    counter := make(chan int)
    even := make(chan int)
    odd := make(chan int)
    square := make(chan int)
    merge := make(chan int)
    out := make(chan struct{})

    //GoRoutines
    go counterFn(counter)
    go squarerFn(counter, square)
    go counterSplit(counter, even, odd)
    go merger(square, odd, merge)
    go printOut(merge, out)
    <-out
}

func counterFn(counter chan int) {
    for i := 0; i < 5; i++ {
        counter <- i
    }
    close(counter)
}

func squarerFn(counter chan int, square chan int) {
    for i := range counter {
        square <- i * i
    }
    close(square)
}

func counterSplit(counter chan int, even chan int, odd chan int) {
    for i := range counter {
        if i%2 == 0 {
            even <- i
        } else {
            odd <- i
        }
    }
    close(even)
    close(odd)
}

func merger(even chan int, odd chan int, merge chan int) {
    i := 0
    for {
        fmt.Printf("%d \n", i)
        a, ok := <-even
        if !ok {
            i++
        } else {
            merge <- a
        }
        a, ok = <-odd
        if !ok {
            i++
        } else {
            merge <- a
        }
        if i == 2 {
            break
        }
    }
    close(merge)
}

func printOut(merge chan int, out chan struct{}) {
    for i := range merge {
        fmt.Print(i)
    }
    close(out)
}

Here is the output

go run channel_pipelineAdv.go
0 , 0
1
0 , 9
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        D:/channel_pipelineAdv.go:20 +0x233

goroutine 7 [chan send]:
main.squarerFn(0x0?, 0x0?)
        D:/channel_pipelineAdv.go:32 +0x45
created by main.main in goroutine 1
        D:/channel_pipelineAdv.go:16 +0x114

goroutine 8 [chan send]:
main.counterSplit(0x0?, 0x0?, 0x0?)
        D:/channel_pipelineAdv.go:40 +0x5e
created by main.main in goroutine 1
        D:/channel_pipelineAdv.go:17 +0x179

goroutine 9 [chan receive]:
main.merger(0x0?, 0x0?, 0x0?)
        D:/channel_pipelineAdv.go:59 +0xca
created by main.main in goroutine 1
        D:/channel_pipelineAdv.go:18 +0x1d9

goroutine 10 [chan receive]:
main.printOut(0x0?, 0x0?)
        D:/channel_pipelineAdv.go:73 +0x77
created by main.main in goroutine 1
        D:/channel_pipelineAdv.go:19 +0x227
exit status 2
PS D:\Study\Go\GoLearn\basic> 

Thanks in advance


Solution

  • I'd say the code of merger is problematic: its relying on incrementing of i up to a limit of 2 appears to be based on the expectation that both even and odd are closed roughly at the same time, and then a read from even returning false as its second value will be followed by a read from odd–also returning false as its second value.

    This expectation is unfounded: any goroutine might get "stalled" for whatever reason which would make other goroutines do "more" progress.

    Think what happens if the goroutine sending to odd gets way less computing quanta than that sending to even. In this scenario, suppose that even is closed, and odd is not and there will be two or more sends (and matching receives) performed on it. The loop in merger will execute a "no-op" receive from the closed even and increment i by one, then a "true" receive from odd (and not touch i), then another successful "no-op" receive from even and bump i to 2, and then another "true" receive from odd and then exit as i is 2 even though there may be more sends to be done over odd.

    We could fix your code by employing an idiom of using the switch statement operating on more than a single channel in a loop combined with "disabling" the channels which are "known to be done with":

    package main
    
    import "fmt"
    
    func main() {
        // create & consume channel
        counter := make(chan int)
        even := make(chan int)
        odd := make(chan int)
        square := make(chan int)
        merge := make(chan int)
        out := make(chan struct{})
    
        //GoRoutines
        go counterFn(counter)
        go squarerFn(counter, square)
        go counterSplit(counter, even, odd)
        go merger(square, odd, merge)
        go printOut(merge, out)
        <-out
    }
    
    func counterFn(counter chan int) {
        for i := 0; i < 5; i++ {
            counter <- i
        }
        close(counter)
    }
    
    func squarerFn(counter chan int, square chan int) {
        for i := range counter {
            square <- i * i
        }
        close(square)
    }
    
    func counterSplit(counter chan int, even chan int, odd chan int) {
        for i := range counter {
            if i%2 == 0 {
                even <- i
            } else {
                odd <- i
            }
        }
        close(even)
        close(odd)
    }
    
    func merger(even chan int, odd chan int, merge chan int) {
        for {
            select {
            case a, ok := <-even:
                if ok {
                    merge <- a
                } else {
                    even = nil
                }
            case b, ok := <-odd:
                if ok {
                    merge <- b
                } else {
                    odd = nil
                }
            default:
                close(merge)
                return
            }
        }
    }
    
    func printOut(merge chan int, out chan struct{}) {
        for i := range merge {
            fmt.Print(i)
        }
        close(out)
    }
    

    (Playground.)

    Note that there, once we're done with even, we overwrite it with nil which will make the next attempt to read from it block indefinitely, in effect making select always prefer another branch–if any. Eventually disabling odd in the same way (by now, you should understand that odd might as well get closed earlier than even, I'm just continuing the first part of the explanation) will make select prefer the only remaining branch, default, which will make it close the resulting channel and exit.