Skip to content

Deadlock & Race Conditions

Deadlocks and Race Conditions in Go

Concurrency brings challenges like deadlocks and race conditions, which can lead to improper program behavior or complete system failure.


1. Deadlocks

A deadlock occurs when two or more goroutines wait indefinitely for each other to release a resource, and none can proceed.

Common Causes of Deadlocks

  • Holding multiple locks in inconsistent order.
  • Goroutines waiting on each other to signal indefinitely.
  • Forgetting to release locks or send signals.

Example: Deadlock with Mutex

package main

import (
    "fmt"
    "sync"
)

func main() {
    var mu sync.Mutex

    // Goroutine 1 locks and waits
    go func() {
        mu.Lock()
        defer mu.Unlock()

        fmt.Println("Goroutine 1 locked")
        // Simulate some work
    }()

    // Main goroutine locks and waits
    mu.Lock()
    defer mu.Unlock()

    fmt.Println("Main goroutine locked")
}

Output:
The program deadlocks because both goroutines are waiting for each other to unlock.


How to Prevent Deadlocks

  1. Avoid nested locks.
    Minimize locking multiple resources simultaneously.
  2. Use channels.
    Channels can help avoid explicit locking mechanisms.
  3. Lock in a consistent order.
    Always acquire locks in the same sequence to prevent cyclic waits.
  4. Use sync.TryLock.
    Avoid blocking indefinitely when acquiring a lock.

Example: Resolving Deadlock with Channels

package main

import "fmt"

func main() {
    ch := make(chan int)

    // Send and receive in separate goroutines
    go func() {
        ch <- 1
    }()

    val := <-ch
    fmt.Println("Received value:", val)
}

2. Race Conditions

A race condition occurs when multiple goroutines access shared resources concurrently, and at least one of them modifies the data, leading to unpredictable outcomes.

Detecting Race Conditions

  • Go Race Detector
    Run your program with go run -race .; It reports race conditions in real-time.

Example: Race Condition

package main

import (
    "fmt"
    "time"
)

var counter int

func increment() {
    for i := 0; i < 1000; i++ {
        counter++
    }
}

func main() {
    go increment()
    go increment()

    time.Sleep(time.Second) // Wait for goroutines to complete
    fmt.Println("Final Counter:", counter)
}

Output:
The final counter value varies on different executions due to simultaneous modifications by multiple goroutines.


How to Prevent Race Conditions

  1. Use Mutex:
    Protect critical sections using sync.Mutex.
  2. Use Atomic Operations:
    Leverage sync/atomic for low-level thread-safe operations.
  3. Use Channels:
    Pass data between goroutines via channels instead of shared variables.

Example: Resolving Race with Mutex

package main

import (
    "fmt"
    "sync"
)

var counter int
var mu sync.Mutex

func increment() {
    for i := 0; i < 1000; i++ {
        mu.Lock()
        counter++
        mu.Unlock()
    }
}

func main() {
    var wg sync.WaitGroup

    wg.Add(2)
    go func() {
        defer wg.Done()
        increment()
    }()
    go func() {
        defer wg.Done()
        increment()
    }()

    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

Example: Resolving Race with Atomic Operations

package main

import (
    "fmt"
    "sync/atomic"
)

var counter int64

func increment() {
    for i := 0; i < 1000; i++ {
        atomic.AddInt64(&counter, 1)
    }
}

func main() {
    done := make(chan bool, 2)

    go func() {
        increment()
        done <- true
    }()
    go func() {
        increment()
        done <- true
    }()

    <-done
    <-done
    fmt.Println("Final Counter:", counter)
}

Key Differences: Deadlock vs Race Condition

Aspect Deadlock Race Condition
Cause Waiting indefinitely for resource release. Concurrent access to shared resources.
Symptom Program hangs or freezes. Program exhibits unpredictable behavior.
Detection Easily reproducible with static analysis. Requires dynamic analysis (go run -race .).
Solution Proper locking, ordering, or avoiding cycles. Synchronization via locks, channels, or atomic.

Best Practices to Avoid Both

  1. Use Go's Race Detector: Regularly test using -race to catch potential issues.
  2. Prefer Channels Over Locks: Channels often simplify communication without explicit synchronization.
  3. Minimize Shared State: Design your program to avoid sharing data among goroutines whenever possible.
  4. Document Goroutine Interaction: Ensure clear documentation of how goroutines coordinate and share resources.