Deadlock Detection in Goroutine Communication
Illustrates deadlock-prone channel patterns and a detector that uses timeouts to surface stuck goroutines.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
package main
import (
"context"
"fmt"
"sync"
"time"
)
// DeadlockDetector monitors goroutine communication for deadlocks
type DeadlockDetector struct {
timeout time.Duration
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
// NewDeadlockDetector creates a new detector with specified timeout
func NewDeadlockDetector(timeout time.Duration) *DeadlockDetector {
ctx, cancel := context.WithCancel(context.Background())
return &DeadlockDetector{
timeout: timeout,
ctx: ctx,
cancel: cancel,
}
}
// MonitorGoroutine tracks a goroutine for potential deadlock
func (d *DeadlockDetector) MonitorGoroutine(name string, fn func()) error {
d.wg.Add(1)
errChan := make(chan error, 1)
go func() {
defer d.wg.Done()
defer close(errChan)
// Create timeout context for this goroutine
ctx, cancel := context.WithTimeout(d.ctx, d.timeout)
defer cancel()
// Channel to track completion
done := make(chan struct{}, 1)
// Run the target function in a separate goroutine
go func() {
fn()
done <- struct{}{}
}()
// Wait for either completion or timeout
select {
case <-done:
// Success - no deadlock
return
case <-ctx.Done():
errChan <- fmt.Errorf("goroutine '%s' deadlock detected: %w", name, ctx.Err())
return
}
}()
// Wait for result or parent cancellation
select {
case err := <-errChan:
return err
case <-d.ctx.Done():
return fmt.Errorf("monitoring cancelled for goroutine '%s': %w", name, d.ctx.Err())
}
}
// Stop terminates all monitoring and waits for cleanup
func (d *DeadlockDetector) Stop() {
d.cancel()
d.wg.Wait()
}
// Example deadlock scenario: unbuffered channel send without receiver
func deadlockExample() {
ch := make(chan int)
// This will deadlock - sending to unbuffered channel with no receiver
ch <- 42
}
// Fixed version with proper synchronization
func noDeadlockExample() {
ch := make(chan int, 1) // Buffered channel prevents deadlock
ch <- 42
close(ch)
}
func main() {
// Create detector with 2-second timeout
detector := NewDeadlockDetector(2 * time.Second)
defer detector.Stop()
// Test deadlock scenario
fmt.Println("Testing deadlock scenario...")
err := detector.MonitorGoroutine("deadlock-example", deadlockExample)
if err != nil {
fmt.Printf("Error: %v\n", err) // Should show deadlock error
}
// Test non-deadlock scenario
fmt.Println("\nTesting non-deadlock scenario...")
err = detector.MonitorGoroutine("no-deadlock-example", noDeadlockExample)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("No deadlock detected!")
}
}How It Works
Illustrates deadlock-prone channel patterns and a detector that uses timeouts to surface stuck goroutines.
Runs goroutines that intentionally block on channels, wraps operations in select with context deadlines, and reports when operations exceed expected durations to highlight deadlocks.
Key Concepts
- 1Shows classic send or receive ordering issues that freeze pipelines.
- 2Context with timeout guards channel operations.
- 3Detector prints diagnostics when deadlines hit.
When to Use This Pattern
- Teaching concurrency pitfalls.
- Adding watchdogs around channel-based systems.
- Reproducing deadlocks in tests to validate fixes.
Best Practices
- Always consider select with timeout when dealing with channels.
- Avoid holding locks while performing blocking channel operations.
- Prefer bounded buffers to reduce risk of circular waits.
Go Version1.17
Difficultyadvanced
Production ReadyYes
Lines of Code110