curiousengineer
curiousengineer

Reputation: 2625

Buffered channel and closing it

I have a snippet of code that I am trying to understand based on how I put the close call and the location

func main() {
    ch := make(chan int, 2)

    go func(ch chan int) {
        for i := 1; i <= 5; i++ {
            ch <- i
            fmt.Println("Func goroutine sends data: ", i)
        }
        //Pos1 - Works perfectly
        //close(ch)
    }(ch)

    fmt.Println("Main goroutine sleeps 2 seconds")
    time.Sleep(time.Second * 2)

    fmt.Println("Main goroutine begins receiving data")
    //Pos2 - Puts in only 2 ints 1 and 2 and then prints only that
    //close(ch)
    for d := range ch {
        fmt.Println("Main goroutine received data:", d)
    }
    //Pos3 - Throws fatal error
    close(ch)
}

I have been trying to understand and read blogs on this but not able to understand somethings still

  1. When I place the close at Pos1, it works fine. But I am not sure why it works. The buffer cannot hold more than 2 elements at any given time, so when 2 elements are written, the loop will block until the main routing does a read. But I thought to do a range over a buffered channel, the range function has to know beforehand how many elements to iterate over and for that channel must be closed. Why does close work at this position?
  2. When I put it as position 2, it prints only 2 elements which kind of makes sense but why is the for loop not throwing an exception when it is trying to write more elements to the channel that is closed?
  3. When I close at Pos3, I get an exception fatal error: all goroutines are asleep - deadlock! though all 5 ints are printed. Why is that?

Upvotes: 1

Views: 4713

Answers (2)

Ambareesh
Ambareesh

Reputation: 363

Modifying the code snippets with some peppered print statements to add on to the excellent answer by @leaf bebop, to clarify what's happening.

Pos 2, but make main wait a bit

func main() {
    ch := make(chan int, 2)

    go func(ch chan int) {
        for i := 1; i <= 5; i++ {
            ch <- i
            fmt.Println("Func goroutine sends data: ", i)
        }
        fmt.Println("goR work is done")
    }(ch)

    fmt.Println("Main goroutine sleeps 2 seconds")
    time.Sleep(time.Second * 2)

    fmt.Println("Main goroutine begins receiving data")
    close(ch)
    for d := range ch {
        fmt.Println("Main goroutine received data:", d)
    }

    fmt.Println("Main waiting, will the other goR panic?")
    time.Sleep(time.Second * 10)
}

Main goroutine sleeps 2 seconds
Func goroutine sends data:  1
Func goroutine sends data:  2
Main goroutine begins receiving data
Main goroutine received data: 1
panic: send on closed channel

goroutine 6 [running]:
main.main.func1(0x0?)
    /tmp/sandbox3746599466/prog.go:15 +0x3b
created by main.main
    /tmp/sandbox3746599466/prog.go:13 +0x85

Program exited.

So, given enough time (main routine didn't exit), the scheduler went back to the go routine, where we wrote to a closed (closed by main) channel - Panic!

Pos 2, but only send 2 values, i.e not writing to a closed channel


func main() {
    ch := make(chan int, 2)

    go func(ch chan int) {
        // Send only 2 values
        for i := 1; i <= 2; i++ {
            ch <- i
            fmt.Println("Func goroutine sends data: ", i)
        }
        fmt.Println("goR work is done")
    }(ch)

    fmt.Println("Main goroutine sleeps 2 seconds")
    time.Sleep(time.Second * 2)

    fmt.Println("Main goroutine begins receiving data")
    close(ch)
    for d := range ch {
        fmt.Println("Main goroutine received data:", d)
    }

    fmt.Println("Main waiting, will the other goR panic?")
    time.Sleep(time.Second * 10)
    fmt.Println("Main exiting")
}

Main goroutine sleeps 2 seconds
Func goroutine sends data:  1
Func goroutine sends data:  2
goR work is done
Main goroutine begins receiving data
Main goroutine received data: 1
Main goroutine received data: 2
Main waiting, will the other goR panic?
Main exiting

Program exited.

No panic, no spinning for ever. Main routine just exits.

Pos 3, check if main got unblocked?

func main() {
    ch := make(chan int, 2)

    go func(ch chan int) {
        for i := 1; i <= 5; i++ {
            ch <- i
            fmt.Println("Func goroutine sends data: ", i)

        }
        fmt.Println("Routine done with work")
    }(ch)

    fmt.Println("Main goroutine sleeps 2 seconds")
    time.Sleep(time.Second * 2)

    fmt.Println("Main goroutine begins receiving data")
    for d := range ch {
        fmt.Println("Main goroutine received data:", d)
    }

    fmt.Println("Have I closed this channel?")
    close(ch)
    fmt.Println("I have")
}


Main goroutine sleeps 2 seconds
Func goroutine sends data:  1
Func goroutine sends data:  2
Main goroutine begins receiving data
Main goroutine received data: 1
Main goroutine received data: 2
Main goroutine received data: 3
Func goroutine sends data:  3
Func goroutine sends data:  4
Func goroutine sends data:  5
Routine done with work
Main goroutine received data: 4
Main goroutine received data: 5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    /tmp/sandbox3301646152/prog.go:26 +0x194

Program exited.

Main never got even to close the channel, it was blocked in the range loop. It'll stay here forever, until it gets a value, or someone closes the channel. We don't have any other routine to do that, so deadlock.

Upvotes: 0

leaf bebop
leaf bebop

Reputation: 8232

But I thought to do a range over a buffered channel, the range function has to know beforehand how many elements to iterate over and for that channel must be closed. 

That assumption is wrong and the root of all misunderstandings.

The behavior of ranging over a channel is described in the Go Spec: https://golang.org/ref/spec#For_statements

For channels, the iteration values produced are the successive values sent on the channel until the channel is closed. If the channel is nil, the range expression blocks forever.

The channel does not need to be closed when the for statement is evaluated and the statement does not need to know number of elements.

So, in your code, when you put close in Pos1, it is indeed the right way to do it. When you put it in Pos3, the for loop waits the channel to be closed, which can only happen after the for loop itself, so it is a deadlock.

Putting close in Pos2 is buggy and the behavior is a little tricky. It is possible to raise an error, yet it is possible to just output two numbers. This is because when the channel is closed before the for loop, the loop can run without block and then main() returns. And when main() returns, the Go program ends. Whether it raises an error solely depends on if the scheduler switch to goroutine between the process, which is not guaranteed.

Upvotes: 6

Related Questions