SoFunction
Updated on 2025-03-05

Solve the problem of Go language deadlock and goroutine leakage

When will it cause a deadlock

It has been said in the principle of computer composition that there are three necessary conditions for deadlock: loop waiting, resource sharing, and non-preemptive. There are only two cases of channel deadlock in concurrency:

  • Data is to be sent, but no one receives it
  • Data is to be received, but no one sends it

Deadlock when sending a single value

It will be clear to remember these two points. Reviewing the previous examples will make you dead

a := make(chan int)
a <- 1   //Write data to channelz := <-a //fromchannelRead data in
  • Unbuffered channel when there is and only one coroutine
  • Sending first will block at sending, and receiving first will block at receiving.
  • The sending operation is blocked before the receiver is ready, and the receiving operation is blocked before the sending,

The solution is to change to a buffer channel, or use coroutine pairing

Solution 1: Coroutine pairing, send or receive first does not matter

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 inside the buffer channel can be tested using the len() function.
  • The capacity of the buffer channel can be tested with cap()
  • When cap>len is satisfied, the sending will not block because it is not full.
  • When len>0, the reception will not block because it is not empty.

Using buffer channels allows producers and consumers to reduce the possibility of blockage and is more friendly to asynchronous operations without waiting for the other party to prepare, but the capacity should not be set too large, otherwise it will consume more memory.

Deadlock sent by multiple values

Pairing can make the deadlock disappear, but when multiple values ​​are sent, it will be unable to match, and it will be deadlocked again.

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

As expected, deadlocked

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
()

In work, only the notification signal is one-to-one, and it will no longer be used after notification. Other situations such as multiple read and write pairing will not exist at all.

Solve multi-value send deadlock

More commonly, loops are used to continuously receive values, accept one and process one, as follows:

func multipleLoop() {
 chanInt := make(chan int)
 defer close(chanInt)
 go func() {
  for {
   //If you don't use ok, it will leak goroutine   //res := &lt;-chanInt
   res,ok := &lt;-chanInt
   if !ok {
                 break
            }
   (res)
  }
 }()
 chanInt &lt;- 1
 chanInt &lt;- 1
}

Output:

1
1

  • Add a binary value to the reception of the channel. OK means whether the channel is normal. If it is closed, it will be false.
  • You can delete that logic and try to output a sequence like 1 2 0 0 0, because closing takes time, and the closed channel obtained by loop receiving is 0
  • About goroutine leaks will be discussed later

Should it be sent or received first

What will happen if we change the position, 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
  }
  (res)
 }
}

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

goroutine 1 [chan receive]:
main.multipleDeathLock2()

  • The above result appears because the for loop keeps getting the value in the channel, but after reading 1 2, no new value is passed in the channel, so the receiver is blocked.
  • Why can I receive it first and then send it? Because after sending it ends early, the defer of the function will automatically close the channel.
  • So we should always receive and then send, and the sender should close it

goroutine leak

There are three scenarios where goroutine terminates:

  • When a goroutine does its job
  • Due to an unprocessed error
  • There are other coroutines that tell it to terminate

When none of the three conditions are met, goroutine will continue to run

func goroutineLeak() {
 chanInt := make(chan int)
 defer close(chanInt)
 go func() {
  for {
            res := <-chanInt
   //res,ok := <-chanInt
   //if !ok {
            //     break
            //}
   (res)
  }
 }()
 chanInt <- 1
 chanInt <- 1
}

The above goroutineLeak() function triggers defer close(chanInt) to close the channel
However, the goroutine in the anonymous function is not closed, but keeps looping to get the value and gets the closed channel value (here is the default value of int 0)
The goroutine will run forever, and if used again in the future, a new leak will appear! Causes more and more memory and CPU usage

Output, if the program does not stop, it will continue to output 0.
1
1
0
0
0
...

If it is not closed and there is no external write value, the receiving place will be blocked there forever, and there will be no output.

func goroutineLeakNoClosed() {
 chanInt := make(chan int)
 go func() {
  for {
            res := <-chanInt
   (res)
  }
 }()
}
  • No blocking of any output
  • The same goes for writing
  • If it is a buffered channel, it is the same if it is replaced with a full channel and has not read it; or if it is replaced with a empty channel and has not written it.
  • In addition to blocking, goroutine enters the dead loop is also the reason for the leakage.

How to find a leak

  • Use the pprof monitoring tool that comes with golang to discover memory increases. This will be discussed later
  • You can also monitor the memory usage of the process, such as the process-exporter provided by prometheus
  • If you have a memory leak/goroutine leak code scanning tool, please leave a message, thank you!

summary

Today we have learned some details, but very important knowledge points, which are also high-frequency questions for future interviews!

  • If it is a signal, it should be guaranteed to correspond one by one, otherwise it will be locked
  • In addition to signaling, we usually use loop processing channels to continuously process data during work.
  • It should always be received first and then sent, and closed by the sender, otherwise it will be prone to deadlock or leak
  • At the receiving location, you should make a judgment on whether the channel is closed. If it is closed, you should exit the reception, otherwise it will be leaked.
  • Be careful of goroutine leakage. You should check the channel and exit it in time when the channel is closed.
  • In addition to blocking, goroutine enters the dead loop is also the reason for the leakage.

Source code address of this section

This is the article about Go language deadlock and goroutine leaks. For more information about Go language deadlock and goroutine leaks, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!