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