SoFunction
Updated on 2025-03-01

Case analysis of deadlock and live lock in Go concurrent programming

What is a deadlock, a live lock

What is deadlock: In a concurrent program, two or more threads wait for each other to complete the operation, resulting in them being blocked and waiting for each other to complete indefinitely. In this case, the program will be stuck and cannot continue to execute.

What is a live lock: It means that the program is running all the time, but it cannot make progress. For example, in some cases, multiple threads compete for the same resource, and each thread releases the resource so that other threads can use it. However, without proper synchronization, these threads may try to get the resource at the same time and then release it again. This may cause threads to run in infinite loops without making progress.

Case analysis of deadlock

1. Write code that will cause deadlocks:

package main

import (
 "fmt"
 "sync"
)

func main() {
 var mu 
 ()
 defer ()

 wg := {}
 (1)
 go func() {
  ("goroutine started")
  () // The lock was obtained here  ("goroutine finished")
  ()
  ()
 }()

 ()
}

Run and output:

[root@workhost temp02]# go run  
goroutine started
fatal error: all goroutines are asleep - deadlock! # The error is very obvious, telling you that it is deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000010030?)
        /usr/local/go/src/runtime/:62 +0x27
...
...

The above code uses mutex to implement a mutex. The main goroutine acquires the lock and starts a new goroutine. The new goroutine also tries to acquire the lock to perform its tasks. However, since the main goroutine does not release the lock, the new goroutine will wait for the lock, resulting in a deadlock.

2. Code transformation

In the above code, you can solve the problem by moving the defer () in the main goroutine to the () in the goroutine function. This way, when the goroutine acquires the lock, it can release the lock after completing the task so that the main goroutine can continue to execute.

Renovated code:

package main

import (
 "fmt"
 "sync"
)

func main() {
 var mu 
 ()
 wg := {}
 (1)
 go func() {
  ("goroutine started")
  () // The lock was obtained here  ("goroutine finished")
  ()
  ()
 }()
 () // Release the lock ()
}

Run and output:

[root@workhost temp02]# go run  
goroutine started
goroutine finished

3. How to avoid deadlocks

In Go language, to avoid deadlocks, you must be clear about the following rules:

  • Avoid nested locks: When using multiple locks, make sure they are nested in the same order. Otherwise, a loop waiting situation may occur, resulting in deadlock.
  • Avoid infinite waiting: If a timeout is specified when acquiring the lock, make sure that errors can be handled or other actions can be performed after the timeout.
  • Avoid excessive competition: If multiple coroutines need to access the same resources, make sure they do not interfere with each other. Mechanisms such as mutex locks or read and write locks can be used to solve competition problems.
  • Using Channels: Channels in Go can be used to coordinate concurrent operations. Using channels to deliver messages and synchronous operations can avoid deadlocks and competition issues.
  • Ensure resource release: When using locks or other resources, be sure to make sure they are released after use, otherwise it may cause deadlocks.
  • Use the select statement: When using a channel for concurrent operations, you can use the select statement to avoid deadlocks. Select one of multiple channels to operate through the select statement, which can prevent deadlocks when a channel is blocked.

Case analysis of life locks

1. Write code that will issue life locks:

package main

import (
 "fmt"
 "sync"
)

func main() {
 var wg 
 var mu 
 var flag bool

 (2)

 // goroutine 1
 go func() {
  // Get the lock resource first  ("goroutine 1 get mu")
  ()
  defer ()

  // Then wait for the value of the flag variable to become true  ("goroutine 1 waiting sign")
  for !flag {
   // Continuously waiting  }

  // The final output and release the lock resource  ("goroutine 1 Released from waiting")
  ()
 }()

 // goroutine 2
 go func() {
  // Get the lock resource first  ("goroutine 2 get mu")
  ()
  defer ()

  // Then wait for the value of the flag variable to become true  ("GoRoutine2 Waiting for Sign")
  for !flag {
   // Continuously waiting  }

  // The final output and release the lock resource  ("GoRoutine 2 Released from waiting")
  ()
 }()

 // Wait 1 second in the main thread so that two goroutines start waiting for the value of the flag variable // Then set the flag variable to true // Since two goroutines will wake up at the same time and try to acquire lock resources, they will wait for each other // The problem of live locks ultimately, and none of them could move forward ("Main thread sleeps 1 second")
 ("Both goroutines should wait for the flag")
 flag = true
 ()

 ("All GoRoutines Completed")
}

Run and output:

[root@workhost temp02]# go run  
Main thread sleeps 1 second
Both goroutines should wait for the flag
goroutine 2 get mu
GoRoutine2 Waiting Sign
GoRoutine 2 Released from waiting
goroutine 1 get mu
goroutine 1 waiting sign
goroutine 1 Released from waiting
All GoRoutines completed

There is a live lock problem in the above code. If two goroutines wait for the flag to become true at the same time and both have acquired the lock resource, they will enter a dead loop and wait for each other and cannot continue to move forward.

2. Code transformation

Renovated code:

package main

import (
 "fmt"
 "runtime"
 "sync"
)

func main() {
 var wg 
 var mu 
 var flag bool

 (2)

 // goroutine 1
 go func() {
  // Get the lock resource first  ("goroutine 1 get mu")
  ()
  defer ()

  // Then wait for the value of the flag variable to become true  ("goroutine 1 waiting sign")
  for !flag {
   () // Giving up the time film  }

  // The final output and release the lock resource  ("goroutine 1 Released from waiting")
  ()
 }()

 // goroutine 2
 go func() {
  // Get the lock resource first  ("goroutine 2 get mu")
  ()
  defer ()

  // Then wait for the value of the flag variable to become true  ("GoRoutine2 Waiting for Sign")
  for !flag {
   () // Giving up the time film  }

  // The final output and release the lock resource  ("GoRoutine 2 Released from waiting")
  ()
 }()

 // Wait 1 second in the main thread so that two goroutines start waiting for the value of the flag variable // Then set the flag variable to true // Since two goroutines will wake up at the same time and try to acquire lock resources, they will wait for each other // The problem of live locks ultimately, and none of them could move forward ("Main thread sleeps 1 second")
 ("Both goroutines should wait for the flag")
 flag = true
 ()

 ("All GoRoutines Completed")
}

The modified code adds a function () to give up the time slice in the loop waiting for the flag variable, so that the two goroutines can abandon the time slice during the waiting period so that other goroutines can execute and obtain lock resources. This method can effectively reduce the degree of competition, thereby avoiding the problem of live locks.

3. How to avoid the possibility of issuing life locks

In concurrent programming in Go language, the key to avoiding live locks is to correctly implement the synchronization mechanism. Here are some ways to avoid live locks:

  • Avoid busy waiting: Use synchronization mechanisms such as channel to implement waiting. This avoids the problem that threads keep occupying CPU resources and cannot make progress.
  • Avoid deadlocks: Deadlocks are often a prerequisite for live locks, so using the lock and synchronization mechanism correctly can avoid deadlocks, thereby avoiding live locks.
  • Reduce the granularity of the lock: reduce the granularity of the lock to a minimum range as much as possible to avoid locking unnecessary code blocks.
  • Use the timeout mechanism: use the TryLock() method or use the select statement to implement the wait timeout mechanism, which can prevent threads from waiting indefinitely.
  • Reasonable design of concurrency models: Reasonable design of concurrency models can avoid problems such as competition and hunger, and thus avoid the occurrence of live locks.

The above is the detailed content of the case analysis of Go concurrent programming deadlock and live lock. For more information about Go deadlock and live lock, please pay attention to my other related articles!