Interview frequency: Go language deadlock and goroutine disclosure

Source location of this section https://github.com/golang-minibear2333/golang/blob/master/4.concurrent/4.4-deadlock/

When will deadlock occur

In the principle of computer composition, it is said that there are three necessary conditions for Deadlock: Circular waiting, resource sharing and non preemptive. There are only two cases of channel deadlock in Concurrency:

  • The data was sent, but no one received it
  • The data needs to be received, but no one sends it

Deadlock when sending a single value

It is clear to keep these two points in mind. Reviewing the previous examples will deadlock

a := make(chan int)
a <- 1   //Write data to channel
z := <-a //Read data from channel
  • When there is only one co process, there is no buffered channel
  • Sending first will block sending, and receiving first will block receiving.
  • The sending operation is blocked before the receiver is ready, and the receiving operation is blocked before sending,
  • The solution is to change to buffered channels or use CO process pairing

Solution 1: the process is paired. It doesn't matter whether you send it first or receive it first. Just pair it

chanInt := make(chan int)
go func() {
    chanInt <- 1
}()

res := <-chanInt

Solution 2: buffer channel

chanInt := make(chan int,1)
chanInt <- 2
res := <-chanInt
  • The number of messages in the buffer channel can be tested by len() function
  • The capacity of the buffer channel can be tested with cap()
  • When cap > len is satisfied, the transmission will not be blocked because it is not full
  • When len > 0, because it is not empty, the reception will not be blocked

Using the buffer channel can reduce the possibility of blocking for producers and consumers, and is more friendly to asynchronous operations without waiting for each other to prepare. However, the capacity should not be set too large, otherwise it will occupy more memory.

Deadlock sent by multiple values

Pairing can make the deadlock disappear, but when multiple values are sent, they cannot be paired, and the deadlock will occur again

func multipleDeathLock() {
	chanInt := make(chan int)
	defer close(chanInt)
    go func() {
		res := <-chanInt
		fmt.Println(res)
	}()
	chanInt <- 1
	chanInt <- 1
}

As expected, it's deadlocked

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.multipleDeathLock()

In operation, only when the notification signal is one-to-one, it will not be used after one notification. Other situations requiring multiple read-write pairing will not exist at all.

Resolve multi value sending deadlock

More commonly, a loop is used to continuously receive values and process one by one, as follows:

func multipleLoop() {
	chanInt := make(chan int)
	defer close(chanInt)
	go func() {
		for {
			//Not using ok will goroutine leak
			//res := <-chanInt
			res,ok := <-chanInt
			if !ok {
                 break
            }
			fmt.Println(res)
		}
	}()
	chanInt <- 1
	chanInt <- 1
}

Output:

1
1
  • Add two values to the reception of the channel. ok represents whether the channel is normal. If it is closed, it is false
  • You can delete that logic and try. It will output a series of numbers such as 1, 2, 0, 0, because closing takes time, and the closed channel received by cyclic reception gets 0
  • The goroutine leak will be discussed later

Should I send or receive first

What happens if we change the location, put the receiver outside and write it inside

func multipleDeathLock2() {
	chanInt := make(chan int)
	defer close(chanInt)
	go func() {
		chanInt <- 1
		chanInt <- 2
	}()
	for {
		res, ok := <-chanInt
		if !ok {
			break
		}
		fmt.Println(res)
	}
}

Output deadlock

1
2
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.multipleDeathLock2()
  • The above result occurs because the for loop keeps getting the value in the channel, but after reading 1 and 2, no new value is passed in the channel, so the receiver is blocked.
  • Why is it OK to receive first and then send, because the defer of the function will be triggered to automatically close the channel after the sending ends in advance
  • Therefore, we should always receive first and then send, and the sender should close it

goroutine leak

goroutine terminates in three scenarios:

  • When a goroutine completes its work
  • An unhandled error occurred
  • There are other processes that tell it to stop

When none of the three conditions are met, goroutine will run all the time

func goroutineLeak() {
	chanInt := make(chan int)
	defer close(chanInt)
	go func() {
		for {
            res := <-chanInt
			//res,ok := <-chanInt
			//if !ok {
            //     break
            //}
			fmt.Println(res)
		}
	}()
	chanInt <- 1
	chanInt <- 1
}
  • After the goroutineLeak() function above completes, trigger defer close(chanInt) to close the channel
  • However, goroutine in the anonymous function is not closed. Instead, it keeps taking a circular value and gets the closed channel value (here is the default value of int 0)
  • goroutine will run forever. If it is used again in the future, there will be new leakage! As a result, more and more memory and cpu are occupied

Output, if the program does not stop, it will always output 0

1
1
0
0
0
...

If it is not turned off and there is no external write value, the receiver will always be blocked there, not even the output

func goroutineLeakNoClosed() {
	chanInt := make(chan int)
	go func() {
		for {
            res := <-chanInt
			fmt.Println(res)
		}
	}()
}
  • No blocking of any output
  • The same is true for writing
  • If it is a buffered channel, replace it with a full channel without reading; Or change to read to the empty channel without writing
  • In addition to blocking, goroutine entering the dead cycle is also the cause of leakage

How to find leaks

Using the pprof monitoring tool provided by golang, you can find the memory increase. This will be described later

You can also monitor the memory usage of processes, such as the process exporter provided by prometheus

If you have a memory leak / goroutine leak code scanning tool, please leave a message and be grateful!

Summary

Today, we learned some details, but very important knowledge, which is also a high-frequency interview question in the future!

  • In case of signal notification, ensure one-to-one correspondence, otherwise it will deadlock
  • In addition to signal notification, we usually use circular processing channels to continuously process data during work
  • It should always be received before sending, and closed by the sender, otherwise it is easy to deadlock or leak
  • At the receiving place, judge whether the channel is closed. If it is closed, exit the receiving, or it will leak
  • Be careful of goroutine leakage. Check the channel and exit when the channel is closed
  • In addition to blocking, goroutine entering the dead cycle is also the cause of leakage

Wonderful review of previous periods

For subsequent article updates, please click to read the original and jump to the open source book

Keywords: Go

Added by raydenl on Sat, 15 Jan 2022 17:23:54 +0200