Skip to content

Latest commit

 

History

History

03_deadlock

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

🛑 Deadlock in Go

💡 Introduction

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.

🚨 Common Causes of Deadlock

  1. Unmatched Send/Receive on Channels: A goroutine tries to send/receive data on a channel without a corresponding receive/send operation.
  2. Improper Use of Mutexes: Two or more goroutines hold locks on shared resources and wait for each other to release them.
  3. Circular Wait: Goroutines create a chain of dependencies on resources that form a circular loop.
  4. Infinite Blocking: A goroutine is blocked indefinitely, for example, using select {} with no cases.

🔍 Common Deadlock Scenarios and Solutions

1. Unmatched Send on a Channel

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
}

2. Improper Use of Mutex

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()
}

3. Circular Wait

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 lock mu2.
  • Goroutine 2 locks mu2, then tries to lock mu1.
  • 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()
}

4. Infinite Blocking with select {}

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")
	}
}

🛠 Best Practices to Avoid Deadlock

  1. Match Sends and Receives:
    Ensure every send operation has a corresponding receive operation, and vice versa.

  2. Lock Resources in a Consistent Order:
    When using multiple locks, always acquire them in the same order across all goroutines.

  3. Use Timeouts:
    Prevent indefinite blocking by using timeouts with select or context.WithTimeout.

  4. Keep Critical Sections Small:
    Minimize the amount of code between Lock() and Unlock() to reduce contention.

  5. Check for Potential Deadlocks:
    Use Go's race detector (go run -race) to identify synchronization issues.

📚 Summary

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.