Why does the use of an unbuffered channel in the same goroutine result in a deadlock?

26,722

Solution 1

From the documentation :

If the channel is unbuffered, the sender blocks until the receiver has received the value. If the channel has a buffer, the sender blocks only until the value has been copied to the buffer; if the buffer is full, this means waiting until some receiver has retrieved a value.

Said otherwise :

  • when a channel is full, the sender waits for another goroutine to make some room by receiving
  • you can see an unbuffered channel as an always full one : there must be another goroutine to take what the sender sends.

This line

c <- 1

blocks because the channel is unbuffered. As there's no other goroutine to receive the value, the situation can't resolve, this is a deadlock.

You can make it not blocking by changing the channel creation to

c := make(chan int, 1) 

so that there's room for one item in the channel before it blocks.

But that's not what concurrency is about. Normally, you wouldn't use a channel without other goroutines to handle what you put inside. You could define a receiving goroutine like this :

func main() {
    c := make(chan int)    
    go func() {
        fmt.Println("received:", <-c)
    }()
    c <- 1   
}

Demonstration

Solution 2

In unbuffered channel writing to channel will not happen until there must be some receiver which is waiting to receive the data, which means in the below example

func main(){
    ch := make(chan int)
    ch <- 10   /* Main routine is Blocked, because there is no routine to receive the value   */
    <- ch
}

Now In case where we have other go routine, the same principle applies

func main(){
  ch :=make(chan int)
  go task(ch)
  ch <-10
}
func task(ch chan int){
   <- ch
}

This will work because task routine is waiting for the data to be consumed before writes happen to unbuffered channel.

To make it more clear, lets swap the order of second and third statements in main function.

func main(){
  ch := make(chan int)
  ch <- 10       /*Blocked: No routine is waiting for the data to be consumed from the channel */
  go task(ch)
}

This will leads to Deadlock

So in short, writes to unbuffered channel happens only when there is some routine waiting to read from channel, else the write operation is blocked forever and leads to deadlock.

NOTE: The same concept applies to buffered channel, but Sender is not blocked until the buffer is full, which means receiver is not necessarily to be synchronized with every write operation.

So if we have buffered channel of size 1, then your above mentioned code will work

func main(){
  ch := make(chan int, 1) /*channel of size 1 */
  ch <-10  /* Not blocked: can put the value in channel buffer */
  <- ch 
}

But if we write more values to above example, then deadlock will happen

func main(){
  ch := make(chan int, 1) /*channel Buffer size 1 */
  ch <- 10
  ch <- 20 /*Blocked: Because Buffer size is already full and no one is waiting to recieve the Data  from channel */
  <- ch
  <- ch
}

Solution 3

In this answer, I will try to explain the error message through which we can peek a little bit into how go works in terms of channels and goroutines

The first example is:

package main

import "fmt"

func main() {
    c := make(chan int)    
    c <- 1   
    fmt.Println(<-c)
}

The error message is:

fatal error: all goroutines are asleep - deadlock!

In the code, there are NO goroutines at all (BTW this error is in runtime, not compile time). When go runs this line c <- 1, it wants to make sure that the message in the channel will be received somewhere (i.e <-c). Go does NOT know if the channel will be received or not at this point. So go will wait for the running goroutines to finish until either one of the following happens:

  1. all of the goroutines are finished(asleep)
  2. one of the goroutine tries to receive the channel

In case #1, go will error out with the message above, since now go KNOWS that there is no way that a goroutine will receive the channel and it need one.

In case #2, the program will continue, since now go KNOWS that this channel is received. This explain the successful case in OP's example.

Share:
26,722
Salah Eddine Taouririt
Author by

Salah Eddine Taouririt

Updated on July 08, 2022

Comments

  • Salah Eddine Taouririt
    Salah Eddine Taouririt almost 2 years

    I'm sure that there is a simple explanation to this trivial situation, but I'm new to the go concurrency model.

    when I run this example

    package main
    
    import "fmt"
    
    func main() {
        c := make(chan int)    
        c <- 1   
        fmt.Println(<-c)
    }
    

    I get this error :

    fatal error: all goroutines are asleep - deadlock!
    
    goroutine 1 [chan send]:
    main.main()
        /home/tarrsalah/src/go/src/github.com/tarrsalah/tour.golang.org/65.go:8 +0x52
    exit status 2
    

    Why ?


    Wrapping c <- in a goroutine makes the example run as we expected

    package main
    
    import "fmt"
    
    func main() {
        c := make(chan int)        
        go func(){
           c <- 1
        }()
        fmt.Println(<-c)
    }
    

    Again, why ?

    Please, I need deep explanation , not just how to eliminate the deadlock and fix the code.