Debugging and Monitoring Synchronization

Effective debugging and monitoring are essential for maintaining reliable concurrent systems.

Race Detection

Go’s race detector is a powerful tool for identifying data races:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

// This function contains a data race
func demonstrateRace() {
	fmt.Println("\n=== Data Race Example ===")
	
	// Shared counter
	counter := 0
	
	// Launch goroutines that increment the counter
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			counter++ // Race condition: read-modify-write without synchronization
		}()
	}
	
	wg.Wait()
	fmt.Printf("Final counter value: %d (expected 1000)\n", counter)
}

// This function fixes the race with a mutex
func demonstrateRaceFixed() {
	fmt.Println("\n=== Data Race Fixed with Mutex ===")
	
	// Shared counter with mutex
	var counter int
	var mu sync.Mutex
	
	// Launch goroutines that increment the counter
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			mu.Lock()
			counter++ // Protected by mutex
			mu.Unlock()
		}()
	}
	
	wg.Wait()
	fmt.Printf("Final counter value: %d (expected 1000)\n", counter)
}

// This function fixes the race with atomic operations
func demonstrateRaceFixedAtomic() {
	fmt.Println("\n=== Data Race Fixed with Atomic Operations ===")
	
	// Shared counter using atomic operations
	var counter int64
	
	// Launch goroutines that increment the counter
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			atomic.AddInt64(&counter, 1) // Atomic increment
		}()
	}
	
	wg.Wait()
	fmt.Printf("Final counter value: %d (expected 1000)\n", counter)
}

func main() {
	// To detect races, run with: go run -race main.go
	demonstrateRace()
	demonstrateRaceFixed()
	demonstrateRaceFixedAtomic()
}

To use the race detector, compile or run your program with the -race flag:

go run -race main.go
go test -race ./...
go build -race main.go

Deadlock Detection

Go provides built-in deadlock detection for goroutines blocked in channel operations, but not for mutex-based deadlocks. Here’s a simple deadlock detector:

package main

import (
	"fmt"
	"sync"
	"time"
)

// DeadlockDetector monitors mutex acquisitions to detect potential deadlocks
type DeadlockDetector struct {
	mu         sync.Mutex
	goroutines map[int64][]string // goroutine ID -> held locks
	locks      map[string]int64   // lock name -> goroutine ID holding it
}

// NewDeadlockDetector creates a new deadlock detector
func NewDeadlockDetector() *DeadlockDetector {
	return &DeadlockDetector{
		goroutines: make(map[int64][]string),
		locks:      make(map[string]int64),
	}
}

// BeforeLock is called before acquiring a lock
func (d *DeadlockDetector) BeforeLock(goroutineID int64, lockName string) {
	d.mu.Lock()
	defer d.mu.Unlock()
	
	// Check if this lock is held by another goroutine
	if holderID, exists := d.locks[lockName]; exists {
		// Check what locks the holder has
		holderLocks := d.goroutines[holderID]
		
		// Check if the holder is waiting for any locks held by this goroutine
		for _, heldLock := range d.goroutines[goroutineID] {
			if holderWaiting, exists := d.locks[heldLock]; exists && holderWaiting == holderID {
				fmt.Printf("POTENTIAL DEADLOCK DETECTED:\n")
				fmt.Printf("  Goroutine %d holds %v and wants %s\n", goroutineID, d.goroutines[goroutineID], lockName)
				fmt.Printf("  Goroutine %d holds %s and wants %v\n", holderID, lockName, holderLocks)
				return
			}
		}
	}
}

// AfterLock is called after acquiring a lock
func (d *DeadlockDetector) AfterLock(goroutineID int64, lockName string) {
	d.mu.Lock()
	defer d.mu.Unlock()
	
	// Record that this goroutine holds this lock
	d.goroutines[goroutineID] = append(d.goroutines[goroutineID], lockName)
	d.locks[lockName] = goroutineID
}

// BeforeUnlock is called before releasing a lock
func (d *DeadlockDetector) BeforeUnlock(goroutineID int64, lockName string) {
	d.mu.Lock()
	defer d.mu.Unlock()
	
	// Remove this lock from the goroutine's held locks
	locks := d.goroutines[goroutineID]
	for i, l := range locks {
		if l == lockName {
			d.goroutines[goroutineID] = append(locks[:i], locks[i+1:]...)
			break
		}
	}
	
	// Remove this lock from the locks map
	delete(d.locks, lockName)
}

// InstrumentedMutex is a mutex with deadlock detection
type InstrumentedMutex struct {
	mu       sync.Mutex
	name     string
	detector *DeadlockDetector
}

// NewInstrumentedMutex creates a new instrumented mutex
func NewInstrumentedMutex(name string, detector *DeadlockDetector) *InstrumentedMutex {
	return &InstrumentedMutex{
		name:     name,
		detector: detector,
	}
}

// Lock acquires the mutex
func (m *InstrumentedMutex) Lock() {
	goroutineID := 123 // In a real implementation, get the actual goroutine ID
	
	m.detector.BeforeLock(goroutineID, m.name)
	m.mu.Lock()
	m.detector.AfterLock(goroutineID, m.name)
}

// Unlock releases the mutex
func (m *InstrumentedMutex) Unlock() {
	goroutineID := 123 // In a real implementation, get the actual goroutine ID
	
	m.detector.BeforeUnlock(goroutineID, m.name)
	m.mu.Unlock()
}

func demonstrateDeadlockDetection() {
	fmt.Println("\n=== Deadlock Detection ===")
	
	// Create a deadlock detector
	detector := NewDeadlockDetector()
	
	// Create instrumented mutexes
	mutex1 := NewInstrumentedMutex("mutex1", detector)
	mutex2 := NewInstrumentedMutex("mutex2", detector)
	
	// Simulate a potential deadlock scenario
	go func() {
		goroutineID := int64(1)
		
		fmt.Println("Goroutine 1: Acquiring mutex1")
		detector.BeforeLock(goroutineID, "mutex1")
		// mutex1.Lock() - simulated
		detector.AfterLock(goroutineID, "mutex1")
		
		time.Sleep(100 * time.Millisecond)
		
		fmt.Println("Goroutine 1: Acquiring mutex2")
		detector.BeforeLock(goroutineID, "mutex2")
		// This would block in a real deadlock
	}()
	
	go func() {
		goroutineID := int64(2)
		
		fmt.Println("Goroutine 2: Acquiring mutex2")
		detector.BeforeLock(goroutineID, "mutex2")
		// mutex2.Lock() - simulated
		detector.AfterLock(goroutineID, "mutex2")
		
		time.Sleep(100 * time.Millisecond)
		
		fmt.Println("Goroutine 2: Acquiring mutex1")
		detector.BeforeLock(goroutineID, "mutex1")
		// This would block in a real deadlock
	}()
	
	// Give time for the simulation to run
	time.Sleep(300 * time.Millisecond)
}

func main() {
	demonstrateDeadlockDetection()
}