为什么以下代码会产生死锁

Golang newbie here. Can somebody explain why the following code generates a deadlock?

I am aware of sending true to boolean <- done channel but I don't want to use it.

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg2 sync.WaitGroup

func producer2(c chan<- int) {
    for i := 0; i < 5; i++ {
        time.Sleep(time.Second * 10)
        fmt.Println("Producer Writing to chan %d", i)
        c <- i
    }
}

func consumer2(c <-chan int) {
    defer wg2.Done()
    fmt.Println("Consumer Got value %d", <-c)

}

func main() {
    c := make(chan int)
    wg2.Add(5)
    fmt.Println("Starting .... 1")
    go producer2(c)
    go consumer2(c)
    fmt.Println("Starting .... 2")

    wg2.Wait()
}

Following is my understanding and I know that it is wrong:

  1. The channel will be blocked the moment 0 is written to it within the loop of producer function
  2. So I expect channel to be emptied by the consumer afterwards.
  3. As the channel is emptied in the step 2, producer function can again put in another value and then get blocked and steps 2 repeats again.

Your original deadlock is caused by wg2.Add(5), you were waiting for 5 goroutines to finish, but only one did; you called wg2.Done() once. Change this to wg2.Add(1), and your program will run without error.

However, I suspect that you intended to consume all the values in the channel not just one as you do. If you change consumer function to:

func consumer2(c <-chan int) {
    defer wg2.Done()
    for i := range c {
        fmt.Printf("Consumer Got value %d
", i)
    }
}

You will get another deadlock because the channel is not closed in producer function, and consumer is waiting for more values that never arrive. Adding close(c) to the producer function will fix it.

Why it error?

Running your code gets the following error:

➜  gochannel go run dl.go
Starting .... 1
Starting .... 2
Producer Writing to chan 0
Consumer Got value 0
Producer Writing to chan 1
fatal error: all goroutines are asleep - deadlock!

Here is why:

There are three goroutines in your code: main,producer2 and consumer2. When it runs,

  • producer2 sends a number 0 to the channel
  • consumer2 recives 0 from the channel, and exits
  • producer2 sends 1 to the channel, but no one is consuming, since consumer2 already exits
  • producer2 is waiting
  • main executes wg2.Wait(), but not all waitgroup are closed. So main is waiting

Two goroutines are waiting here, does nothing, and nothing will be done no matter how long you wait. It is a deadlock! Golang detects it and panic.

There are two concepts you are confused here:

  1. how waitgourp works
  2. how to receive all values from a channel

I'll explain them here briefly, there are alreay many articles out there on the internet.

how waitgroup works

WaitGroup if a way to wait for all groutine to finish. When running goroutines in the background, it's important to know when all of them quits, then certain action can be conducted.

In your case, we run two goroutines, so at the beginning we should set wg2.Add(2), and each goroutine should add wg2.Done() to notify it is done.

Receive data from a channel

When receiving data from a channel. If you know exactly how many data it will send, use for loop this way:

for i:=0; i<N; i++ {
    data = <-c
    process(data)
}

Otherwise use it this way:

for data := range c {
    process(data)
}

Also, Don't forget to close channel when there is no more data to send.

How to fix it?

With the above explanation, the code can be fixed as:

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg2 sync.WaitGroup

func producer2(c chan<- int) {
    defer wg2.Done()
    for i := 0; i < 5; i++ {
        time.Sleep(time.Second * 1)
        fmt.Printf("Producer Writing to chan %d
", i)
        c <- i
    }
    close(c)
}

func consumer2(c <-chan int) {
    defer wg2.Done()
    for i := range c {
        fmt.Printf("Consumer Got value %d
", i)
    }

}

func main() {
    c := make(chan int)
    wg2.Add(2)
    fmt.Println("Starting .... 1")
    go producer2(c)
    go consumer2(c)
    fmt.Println("Starting .... 2")

    wg2.Wait()
}

Here is another possible way to fix it.

The expected output

Fixed code gives the following output:

➜  gochannel go run dl.go
Starting .... 1
Starting .... 2
Producer Writing to chan 0
Consumer Got value 0
Producer Writing to chan 1
Consumer Got value 1
Producer Writing to chan 2
Consumer Got value 2
Producer Writing to chan 3
Consumer Got value 3
Producer Writing to chan 4
Consumer Got value 4