A deadlock occurs in concurrent programming when two or more goroutines are waiting for each other to complete, but none of them can proceed because they are all blocked. This situation typically arises from improper use of shared resources, such as channels, mutexes, or other synchronization tools.
This documentation explores common causes of deadlocks in Go, provides practical examples, and outlines how to prevent them.
- Unmatched Send/Receive on Channels: A goroutine tries to send/receive data on a channel without a corresponding receive/send operation.
- Improper Use of Mutexes: Two or more goroutines hold locks on shared resources and wait for each other to release them.
- Circular Wait: Goroutines create a chain of dependencies on resources that form a circular loop.
- Infinite Blocking: A goroutine is blocked indefinitely, for example, using
select {}
with no cases.
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 1 // Deadlock: no goroutine is ready to receive from this channel
fmt.Println(<-ch) // This line will never be reached
}
Why Deadlock Happens:
The main
goroutine attempts to send a value into the channel ch
, but no other goroutine is available to receive it. As a result, the program is stuck.
Solution:
Perform the send or receive operation in a separate goroutine:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
fmt.Println(<-ch) // Output: 1
}
package main
import "sync"
func main() {
var mu sync.Mutex
mu.Lock() // Deadlock: the mutex is never unlocked
mu.Lock() // Goroutine tries to lock the mutex again, causing a deadlock
mu.Unlock() // This line will never be reached
}
Why Deadlock Happens:
The same goroutine attempts to lock the mutex twice without unlocking it. In Go, a mutex cannot be locked multiple times by the same goroutine.
Solution:
Ensure every Lock()
is followed by an Unlock()
:
package main
import "sync"
func main() {
var mu sync.Mutex
mu.Lock()
// Critical section
mu.Unlock()
mu.Lock() // Lock again after releasing
mu.Unlock()
}
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu1, mu2 sync.Mutex
wg.Add(2)
go func() {
defer wg.Done()
mu1.Lock()
fmt.Println("goroutine 1 acquired lock 1")
mu2.Lock()
fmt.Println("goroutine 1 acquired lock 2")
defer mu1.Unlock()
defer mu2.Unlock()
}()
go func() {
defer wg.Done()
mu2.Lock()
fmt.Println("goroutine 2 acquired lock 2")
mu1.Lock()
fmt.Println("goroutine 2 acquired lock 1")
defer mu1.Unlock()
defer mu2.Unlock()
}()
wg.Wait()
}
Why Deadlock Happens:
- Goroutine 1 locks
mu1
, then tries to lockmu2
. - Goroutine 2 locks
mu2
, then tries to lockmu1
. - Both goroutines are blocked waiting for the other to release the mutexes, creating a circular wait.
Solution:
Always lock resources in a consistent order:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu1, mu2 sync.Mutex
wg.Add(2)
go func() {
defer wg.Done()
mu1.Lock()
fmt.Println("goroutine 1 acquired lock 1")
mu2.Lock()
fmt.Println("goroutine 1 acquired lock 2")
defer mu1.Unlock()
defer mu2.Unlock()
}()
go func() {
defer wg.Done()
mu1.Lock() // Lock in the same order as goroutine 1
fmt.Println("goroutine 2 acquired lock 1")
mu2.Lock()
fmt.Println("goroutine 2 acquired lock 2")
defer mu1.Unlock()
defer mu2.Unlock()
}()
wg.Wait()
}
package main
func main() {
select {} // Deadlock: the main goroutine blocks indefinitely
}
Why Deadlock Happens:
The select {}
statement has no cases to execute, so the main goroutine blocks forever.
Solution:
Add valid case
statements or a default
case:
package main
import "time"
func main() {
select {
case <-time.After(1 * time.Second):
println("Timeout!")
default:
println("Non-blocking operation")
}
}
-
Match Sends and Receives:
Ensure everysend
operation has a correspondingreceive
operation, and vice versa. -
Lock Resources in a Consistent Order:
When using multiple locks, always acquire them in the same order across all goroutines. -
Use Timeouts:
Prevent indefinite blocking by using timeouts withselect
orcontext.WithTimeout
. -
Keep Critical Sections Small:
Minimize the amount of code betweenLock()
andUnlock()
to reduce contention. -
Check for Potential Deadlocks:
Use Go's race detector (go run -race
) to identify synchronization issues.
Deadlock is a critical issue in concurrent programming that can halt your application. Understanding how deadlock arises (e.g., unmatched channel operations, improper use of mutex, or circular wait) and applying preventive measures like consistent locking, timeouts, and careful design can help you build robust concurrent systems.