Learning Objectives
By the end of this tutorial, you will be able to:
- Master happens-before relationships and understand memory ordering guarantees
- Prevent data races using proper synchronization primitives
- Choose the right synchronization tool for your use case
- Build lock-free data structures using atomic operations safely
- Debug concurrent issues with systematic approaches and tools
- Write production-safe concurrent code that works correctly under all conditions
Hook - The Concurrent Programming Challenge
When coordinating a team where multiple people need to read and update the same whiteboard, without clear rules about who can write when and who gets to see what changes, chaos ensues. Go's memory model is exactly that set of rules for concurrent programming—it ensures all goroutines see a consistent view of shared data.
Real-World Impact:
- Cloudflare's 2019 outage: Memory ordering bug caused incorrect request routing globally for 30 minutes
- Uber's payment system bug: Race condition allowed double charging customers
- Financial impact: These issues cost companies millions and damaged customer trust
💡 Key Takeaway: The Go memory model is your contract for safe concurrent programming. It tells you exactly when writes from one goroutine become visible to reads in another goroutine.
Why the Memory Model Matters
Think of the memory model as traffic rules for concurrent programming. Just as traffic rules prevent accidents and ensure smooth flow, memory model rules prevent data races and ensure program correctness.
🎯 Practical Analogy: Imagine a shared kitchen where multiple chefs cook simultaneously. The memory model is like the rule: "Only one chef can use the stove at a time" and "Everyone must see when a dish is finished cooking." Without these rules, you'd have burned food and confused chefs.
The Go memory model specifies the conditions under which reads of a variable in one goroutine are guaranteed to observe values produced by writes to the same variable in another goroutine.
Critical Need for Memory Model Knowledge
- 🔒 Correctness: Prevent data races and undefined behavior that can corrupt data
- 🚀 Performance: Use appropriate synchronization—not too much and not too little
- 🐛 Debugging: Understand why concurrent bugs happen and how to fix them systematically
- 📊 Optimization: Know when compiler and runtime optimizations are safe to apply
Real-world Example: In a high-frequency trading system, understanding the memory model ensures that price updates from one goroutine are immediately visible to trading decision goroutines, preventing stale data trades worth millions.
Real-World Incidents
Cloudflare's Race Condition:
- What happened: Memory ordering bug caused incorrect request routing across their global network
- Impact: 30 minutes of global service degradation affecting millions of users
- Root cause: Misunderstanding of Go's memory model—assuming writes would be visible without proper synchronization
- Fix: Implemented proper mutex synchronization around shared routing data
- Cost: Estimated millions in lost revenue and customer trust
Uber's Payment Bug:
- What happened: Race condition in payment processing allowed double charging
- Impact: Money debited twice from customer accounts, some customers charged thousands instead of hundreds
- Root cause: Concurrent goroutines accessing shared payment state without proper synchronization
- Fix: Added atomic operations and proper locking around payment state transitions
- Cost: $250,000 in refunds plus reputational damage
⚠️ Important: These incidents show that data races aren't just theoretical—they can cause real production issues with financial impact.
Fundamentals of the Memory Model
What is a Memory Model
A memory model defines the contract between programmers and the system about memory access ordering in concurrent programs. It specifies:
- Visibility: When writes from one thread become visible to reads in another
- Ordering: What order operations appear to execute in
- Atomicity: Which operations complete as indivisible units
- Coherence: How different memory locations relate to each other
💡 Key Concept: Without a memory model, compilers and CPUs can reorder operations in ways that break concurrent programs. The memory model defines what reorderings are allowed and what guarantees you have.
Sequential Consistency vs Weak Memory Models
Sequential Consistency (strongest guarantee):
- Operations appear to execute in some total order
- All processors see operations in the same order
- Easy to reason about but expensive to implement
Weak Memory Models (what Go uses):
- Allow reordering for performance
- Provide happens-before relationships instead of total ordering
- More complex but much faster
1// run
2package main
3
4import (
5 "fmt"
6 "runtime"
7 "sync"
8 "time"
9)
10
11// Demonstration of memory ordering effects
12func demonstrateWeakMemory() {
13 var x, y int
14 var a, b int
15
16 iterations := 1000
17 concurrent := 0
18
19 for i := 0; i < iterations; i++ {
20 x, y, a, b = 0, 0, 0, 0
21
22 done := make(chan bool, 2)
23
24 // Goroutine 1: Write x, read y
25 go func() {
26 x = 1
27 a = y
28 done <- true
29 }()
30
31 // Goroutine 2: Write y, read x
32 go func() {
33 y = 1
34 b = x
35 done <- true
36 }()
37
38 <-done
39 <-done
40
41 // In a sequentially consistent model, a=0,b=0 is impossible
42 // But with weak memory ordering, it can happen!
43 if a == 0 && b == 0 {
44 concurrent++
45 }
46 }
47
48 fmt.Printf("Out of %d runs:\n", iterations)
49 fmt.Printf("Concurrent executions (a=0, b=0): %d\n", concurrent)
50 fmt.Printf("This shows weak memory ordering in action\n")
51}
52
53func main() {
54 fmt.Println("=== Memory Ordering Demonstration ===\n")
55 runtime.GOMAXPROCS(4) // Use multiple processors
56 demonstrateWeakMemory()
57}
Memory Barriers and Synchronization
Memory Barriers (or fences) prevent certain types of reordering:
- Compiler barriers: Prevent compiler reordering
- Hardware barriers: Prevent CPU reordering
- Both: Go's synchronization primitives provide both
Synchronization Primitives in Go:
- Channels: Send/receive operations
- Mutexes: Lock/unlock operations
- Atomic operations: Load/store/CAS operations
- sync.Once: One-time initialization
- sync.WaitGroup: Coordination barriers
Happens-Before Relationship
🎯 Practical Analogy: Think of happens-before relationships like a timeline of events in a story. If Chapter 1 happens-before Chapter 2, then everything established in Chapter 1 is available to readers when they start Chapter 2. In Go, this ensures that all writes from one point are visible to reads at later points.
The Core Concept
A happens-before relationship defines when memory operations are guaranteed to be visible across goroutines. Without these relationships, there are no guarantees about order or visibility.
💡 Key Takeaway: If there's no happens-before relationship between two operations, the order in which they appear to execute is undefined, even if they're in the same program!
1// run
2package main
3
4import (
5 "fmt"
6 "time"
7)
8
9var a, b int
10
11// WITHOUT synchronization
12func wrong() {
13 // Goroutine 1
14 go func() {
15 a = 1 // Write 1
16 b = 2 // Write 2
17 }()
18
19 // Goroutine 2
20 go func() {
21 for b == 0 { // Wait for b
22 // Busy wait
23 }
24 // When we get here, is a guaranteed to be 1?
25 // NO! There's no happens-before relationship
26 fmt.Println(a) // Could print 0 or 1
27 }()
28
29 time.Sleep(100 * time.Millisecond)
30}
31
32func main() {
33 wrong()
34}
Correct Synchronization
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9var a, b int
10var mu sync.Mutex
11
12// WITH synchronization
13func correct() {
14 var wg sync.WaitGroup
15 wg.Add(2)
16
17 // Goroutine 1
18 go func() {
19 defer wg.Done()
20 mu.Lock()
21 a = 1
22 b = 2
23 mu.Unlock()
24 }()
25
26 // Goroutine 2
27 go func() {
28 defer wg.Done()
29 mu.Lock()
30 val := b
31 mu.Unlock()
32
33 if val != 0 {
34 mu.Lock()
35 fmt.Println("a =", a) // Guaranteed to be 1
36 mu.Unlock()
37 }
38 }()
39
40 wg.Wait()
41}
42
43func main() {
44 correct()
45}
Formal Happens-Before Rules
The Go memory model defines several happens-before relationships:
Rule 1: Within a single goroutine, operations execute in program order
If statement A appears before statement B in the source code,
then A happens-before B
Rule 2: Send on a channel happens-before receive completes
ch <- value // Send
...
<-ch // Receive sees the value
Rule 3: Receive from an unbuffered channel happens-before send completes
<-ch // Receive
...
ch <- value // Send can complete
Rule 4: Unlock happens-before any subsequent lock
mu.Lock()
// Critical section 1
mu.Unlock() // Unlock happens-before next Lock
mu.Lock()
// Critical section 2: sees effects from critical section 1
mu.Unlock()
Rule 5: Read from atomic variable observes any prior write
atomic.Store(&x, 42) // Write happens-before read
v := atomic.Load(&x) // Sees 42
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9func demonstrateHappensBefore() {
10 var x, y int
11 var wg sync.WaitGroup
12
13 // Rule 1: Within a single goroutine, happens-before order is program order
14 wg.Add(1)
15 go func() {
16 defer wg.Done()
17 x = 1 // Statement A
18 y = 2 // Statement B
19 // A happens-before B
20 }()
21
22 // Rule 2: Send on channel happens-before receive completes
23 ch := make(chan int)
24 wg.Add(2)
25
26 go func() {
27 defer wg.Done()
28 x = 3
29 ch <- 1 // Send happens-before receive
30 }()
31
32 go func() {
33 defer wg.Done()
34 <-ch // Receive
35 fmt.Println(x) // Guaranteed to see x = 3
36 }()
37
38 // Rule 3: Unlock happens-before any subsequent lock
39 var mu sync.Mutex
40 wg.Add(2)
41
42 go func() {
43 defer wg.Done()
44 mu.Lock()
45 x = 4
46 mu.Unlock() // Unlock happens-before next Lock
47 }()
48
49 go func() {
50 defer wg.Done()
51 mu.Lock() // This Lock happens-after above Unlock
52 fmt.Println(x) // Guaranteed to see x = 4
53 mu.Unlock()
54 }()
55
56 wg.Wait()
57}
58
59func main() {
60 demonstrateHappensBefore()
61}
Real-world Example: In a web server, this ensures that when a request handler updates user session data, subsequent requests from the same user see those updates, preventing inconsistent session states.
Transitivity of Happens-Before
If A happens-before B and B happens-before C, then A happens-before C.
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9// Transitivity demonstration
10func demonstrateTransitivity() {
11 var x int
12 ch1 := make(chan int)
13 ch2 := make(chan int)
14
15 // Goroutine A
16 go func() {
17 x = 42 // Operation 1
18 ch1 <- 1 // Operation 2: Signals B
19 }()
20
21 // Goroutine B
22 go func() {
23 <-ch1 // Waits for A (Operation 2 happens-before this)
24 // Now we can see x = 42 (Operation 1 happens-before Operation 2)
25 ch2 <- 1 // Operation 3: Signals C
26 }()
27
28 // Goroutine C
29 go func() {
30 <-ch2 // Waits for B (Operation 3 happens-before this)
31 // Now we can see x = 42 due to transitivity:
32 // Operation 1 happens-before Operation 2
33 // Operation 2 happens-before receive in B
34 // B's send Operation 3 happens-before receive in C
35 fmt.Println("x =", x) // Guaranteed to see 42
36 }()
37
38 // Wait for completion
39 var wg sync.WaitGroup
40 wg.Add(1)
41 go func() {
42 <-ch2
43 wg.Done()
44 }()
45 wg.Wait()
46}
47
48func main() {
49 demonstrateTransitivity()
50}
Synchronization Primitives
🎯 Practical Analogy: Synchronization primitives are like traffic signals for concurrent programming. Just as traffic lights prevent car accidents, these primitives prevent data accidents in concurrent code.
Mutex
💡 Key Takeaway: A mutex is like a bathroom key—only one person can use it at a time, and everyone knows when it's occupied or available.
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9type SafeCounter struct {
10 mu sync.Mutex
11 count int
12}
13
14func (c *SafeCounter) Increment() {
15 c.mu.Lock()
16 c.count++ // Protected by mutex
17 c.mu.Unlock()
18}
19
20func (c *SafeCounter) Value() int {
21 c.mu.Lock()
22 defer c.mu.Unlock()
23 return c.count
24}
25
26func main() {
27 counter := &SafeCounter{}
28 var wg sync.WaitGroup
29
30 // 100 goroutines incrementing
31 for i := 0; i < 100; i++ {
32 wg.Add(1)
33 go func() {
34 defer wg.Done()
35 for j := 0; j < 100; j++ {
36 counter.Increment()
37 }
38 }()
39 }
40
41 wg.Wait()
42 fmt.Println("Count:", counter.Value()) // Always 10000
43}
Real-world Example: In a banking system, mutexes protect account balances during transfers, preventing double-spending. When Alice transfers money to Bob, both accounts are locked until the transfer completes, ensuring no other operations can interfere.
RWMutex
🎯 Practical Analogy: An RWMutex is like a library reference section—many people can read simultaneously, but only one person can write.
💡 Key Takeaway: Use RWMutex when reads are much more frequent than writes. It allows multiple readers to proceed concurrently while still protecting against writer interference.
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "time"
8)
9
10type Cache struct {
11 mu sync.RWMutex
12 data map[string]string
13}
14
15func NewCache() *Cache {
16 return &Cache{
17 data: make(map[string]string),
18 }
19}
20
21// Multiple readers can hold RLock simultaneously
22func (c *Cache) Get(key string) (string, bool) {
23 c.mu.RLock()
24 defer c.mu.RUnlock()
25 val, ok := c.data[key]
26 return val, ok
27}
28
29// Writer needs exclusive Lock
30func (c *Cache) Set(key, value string) {
31 c.mu.Lock()
32 defer c.mu.Unlock()
33 c.data[key] = value
34}
35
36func main() {
37 cache := NewCache()
38 var wg sync.WaitGroup
39
40 // Writer goroutine
41 wg.Add(1)
42 go func() {
43 defer wg.Done()
44 for i := 0; i < 10; i++ {
45 cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
46 time.Sleep(10 * time.Millisecond)
47 }
48 }()
49
50 // Multiple reader goroutines
51 for i := 0; i < 5; i++ {
52 wg.Add(1)
53 go func(id int) {
54 defer wg.Done()
55 for j := 0; j < 20; j++ {
56 if val, ok := cache.Get("key5"); ok {
57 fmt.Printf("Reader %d got: %s\n", id, val)
58 }
59 time.Sleep(5 * time.Millisecond)
60 }
61 }(i)
62 }
63
64 wg.Wait()
65}
Real-world Example: Content delivery networks use RWMutex-like patterns. Millions of users read cached content simultaneously, while content updates happen much less frequently. This allows massive read scalability while still protecting content integrity during updates.
Channels
🎯 Practical Analogy: Channels are like mail carriers with guaranteed delivery. When you send a letter, you know it will be delivered before the recipient acts on it. The "happens-before" guarantee ensures the message is received intact.
💡 Key Takeaway: Channels provide both communication and synchronization. The act of sending and receiving establishes a happens-before relationship, making them perfect for coordinating goroutines.
1// run
2package main
3
4import "fmt"
5
6// Channels provide happens-before guarantees
7
8func channelSynchronization() {
9 // Unbuffered channel: send happens-before receive completes
10 ch1 := make(chan int)
11 var value int
12
13 go func() {
14 value = 42
15 ch1 <- 1 // Send
16 }()
17
18 <-ch1 // Receive happens-after send
19 fmt.Println(value) // Guaranteed to see 42
20
21 // Buffered channel: receive happens-before send completes
22 ch2 := make(chan int, 1)
23 ch2 <- 10
24
25 go func() {
26 <-ch2 // Receive
27 value = 100
28 }()
29
30 ch2 <- 20 // This send blocks until receive happens
31 fmt.Println(value) // Guaranteed to see 100
32}
33
34func main() {
35 channelSynchronization()
36}
Real-world Example: In Kubernetes, channels coordinate between controllers and the API server. When a controller detects a change, the API server processes it, ensuring state changes are applied in the correct order across the cluster.
sync.Once
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9var (
10 instance *Singleton
11 once sync.Once
12)
13
14type Singleton struct {
15 data string
16}
17
18// Thread-safe singleton initialization
19func GetInstance() *Singleton {
20 once.Do(func() {
21 fmt.Println("Initializing singleton...")
22 instance = &Singleton{data: "I'm a singleton!"}
23 })
24 return instance
25}
26
27func main() {
28 var wg sync.WaitGroup
29
30 // Multiple goroutines try to get instance
31 for i := 0; i < 10; i++ {
32 wg.Add(1)
33 go func(id int) {
34 defer wg.Done()
35 inst := GetInstance()
36 fmt.Printf("Goroutine %d got: %s\n", id, inst.data)
37 }(i)
38 }
39
40 wg.Wait()
41 // "Initializing singleton..." printed only once
42}
sync.WaitGroup
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "time"
8)
9
10func worker(id int, wg *sync.WaitGroup) {
11 defer wg.Done() // Decrements counter when done
12
13 fmt.Printf("Worker %d starting\n", id)
14 time.Sleep(time.Second)
15 fmt.Printf("Worker %d done\n", id)
16}
17
18func main() {
19 var wg sync.WaitGroup
20
21 for i := 1; i <= 5; i++ {
22 wg.Add(1) // Increment counter
23 go worker(i, &wg)
24 }
25
26 wg.Wait() // Block until counter reaches 0
27 fmt.Println("All workers completed")
28}
sync.Cond - Condition Variables
Condition variables allow goroutines to wait for specific conditions to become true.
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "time"
8)
9
10type Queue struct {
11 mu sync.Mutex
12 cond *sync.Cond
13 items []int
14}
15
16func NewQueue() *Queue {
17 q := &Queue{
18 items: make([]int, 0),
19 }
20 q.cond = sync.NewCond(&q.mu)
21 return q
22}
23
24func (q *Queue) Enqueue(item int) {
25 q.mu.Lock()
26 defer q.mu.Unlock()
27
28 q.items = append(q.items, item)
29 q.cond.Signal() // Wake up one waiting goroutine
30}
31
32func (q *Queue) Dequeue() int {
33 q.mu.Lock()
34 defer q.mu.Unlock()
35
36 // Wait while queue is empty
37 for len(q.items) == 0 {
38 q.cond.Wait() // Releases lock and waits
39 }
40
41 item := q.items[0]
42 q.items = q.items[1:]
43 return item
44}
45
46func main() {
47 queue := NewQueue()
48 var wg sync.WaitGroup
49
50 // Consumer
51 wg.Add(1)
52 go func() {
53 defer wg.Done()
54 for i := 0; i < 5; i++ {
55 item := queue.Dequeue()
56 fmt.Printf("Consumed: %d\n", item)
57 }
58 }()
59
60 // Producer
61 wg.Add(1)
62 go func() {
63 defer wg.Done()
64 for i := 1; i <= 5; i++ {
65 time.Sleep(500 * time.Millisecond)
66 queue.Enqueue(i)
67 fmt.Printf("Produced: %d\n", i)
68 }
69 }()
70
71 wg.Wait()
72}
Memory Ordering
Sequential Consistency
⚠️ Important: Go does NOT guarantee sequential consistency! This is a critical difference from languages like Java. Without proper synchronization, the compiler and CPU can reorder operations in surprising ways.
🎯 Practical Analogy: Think of memory ordering like a queue at a coffee shop. Without proper rules, people might cut in line, and the order of service becomes unpredictable. Synchronization establishes clear ordering rules.
1// run
2package main
3
4import (
5 "fmt"
6 "runtime"
7 "sync"
8 "sync/atomic"
9)
10
11// WITHOUT atomic
12func withoutAtomic() {
13 var x, y, a, b int
14
15 go func() {
16 x = 1
17 a = y
18 }()
19
20 go func() {
21 y = 1
22 b = x
23 }()
24
25 runtime.Gosched()
26
27 // Possible outcomes:
28 // a=0, b=1
29 // a=1, b=0
30 // a=1, b=1
31 // a=0, b=0 ← This is possible due to reordering!
32
33 fmt.Printf("a=%d, b=%d\n", a, b)
34}
35
36// WITH atomic
37func withAtomic() {
38 var x, y atomic.Int32
39
40 var wg sync.WaitGroup
41 wg.Add(2)
42
43 go func() {
44 defer wg.Done()
45 x.Store(1)
46 a := y.Load()
47 fmt.Printf("a=%d\n", a)
48 }()
49
50 go func() {
51 defer wg.Done()
52 y.Store(1)
53 b := x.Load()
54 fmt.Printf("b=%d\n", b)
55 }()
56
57 wg.Wait()
58}
59
60func main() {
61 fmt.Println("Without atomic:")
62 withoutAtomic()
63
64 fmt.Println("\nWith atomic:")
65 withAtomic()
66}
Real-world Example: In a stock trading system, without proper memory ordering, different threads might see different orderings of trades, leading to inconsistent pricing and regulatory violations. Atomic operations ensure all traders see the same sequence of events.
Atomic Operations
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "sync/atomic"
8)
9
10type Metrics struct {
11 requests atomic.Int64
12 errors atomic.Int64
13}
14
15func (m *Metrics) RecordRequest(success bool) {
16 m.requests.Add(1)
17 if !success {
18 m.errors.Add(1)
19 }
20}
21
22func (m *Metrics) ErrorRate() float64 {
23 reqs := m.requests.Load()
24 errs := m.errors.Load()
25
26 if reqs == 0 {
27 return 0
28 }
29
30 return float64(errs) / float64(reqs)
31}
32
33func main() {
34 metrics := &Metrics{}
35 var wg sync.WaitGroup
36
37 // Simulate requests
38 for i := 0; i < 100; i++ {
39 wg.Add(1)
40 go func(id int) {
41 defer wg.Done()
42 success := id%10 != 0 // 10% error rate
43 metrics.RecordRequest(success)
44 }(i)
45 }
46
47 wg.Wait()
48
49 fmt.Printf("Total requests: %d\n", metrics.requests.Load())
50 fmt.Printf("Total errors: %d\n", metrics.errors.Load())
51 fmt.Printf("Error rate: %.2f%%\n", metrics.ErrorRate()*100)
52}
Memory Order Semantics
Different atomic operations provide different ordering guarantees:
Sequential Consistency (strongest):
- All operations appear in some total order
- All processors agree on this order
- Example:
atomic.LoadInt32,atomic.StoreInt32
Acquire-Release (medium):
- Acquire loads see all prior releases
- Release stores are visible to subsequent acquires
- Example:
sync.Mutex.Lock()(acquire),sync.Mutex.Unlock()(release)
Relaxed (weakest):
- No ordering guarantees beyond atomicity
- Only guarantees individual operation is atomic
- Example: Some internal runtime operations
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "sync/atomic"
8)
9
10type SharedData struct {
11 ready atomic.Bool
12 value int
13}
14
15// Publish-subscribe pattern with proper memory ordering
16func demonstrateAcquireRelease() {
17 data := &SharedData{}
18 var wg sync.WaitGroup
19
20 // Publisher (uses release semantics)
21 wg.Add(1)
22 go func() {
23 defer wg.Done()
24
25 // Prepare data
26 data.value = 42
27
28 // Release: All prior writes are visible after this
29 data.ready.Store(true)
30 fmt.Println("Publisher: Data published")
31 }()
32
33 // Subscriber (uses acquire semantics)
34 wg.Add(1)
35 go func() {
36 defer wg.Done()
37
38 // Acquire: See all writes that happened before release
39 for !data.ready.Load() {
40 // Spin wait
41 }
42
43 fmt.Printf("Subscriber: Read value = %d\n", data.value)
44 }()
45
46 wg.Wait()
47}
48
49func main() {
50 demonstrateAcquireRelease()
51}
Race Condition Prevention
🎯 Practical Analogy: Race conditions are like multiple chefs adding ingredients to the same recipe without coordination—one chef might add salt twice while another skips it entirely, ruining the dish.
Common Race Patterns
⚠️ Important: These are the most common sources of bugs in concurrent Go programs. The race detector is your best friend for catching these early.
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9// WRONG: Map race
10func mapRaceWrong() {
11 m := make(map[int]int)
12
13 for i := 0; i < 100; i++ {
14 go func(n int) {
15 m[n] = n // DATA RACE!
16 }(i)
17 }
18}
19
20// CORRECT: Map with mutex
21func mapRaceFixed() {
22 m := make(map[int]int)
23 var mu sync.Mutex
24
25 var wg sync.WaitGroup
26 for i := 0; i < 100; i++ {
27 wg.Add(1)
28 go func(n int) {
29 defer wg.Done()
30 mu.Lock()
31 m[n] = n // Protected
32 mu.Unlock()
33 }(i)
34 }
35 wg.Wait()
36
37 fmt.Println("Map size:", len(m))
38}
39
40// WRONG: Slice race
41func sliceRaceWrong() {
42 var slice []int
43
44 for i := 0; i < 100; i++ {
45 go func(n int) {
46 slice = append(slice, n) // DATA RACE!
47 }(i)
48 }
49}
50
51// CORRECT: Slice with mutex
52func sliceRaceFixed() {
53 var slice []int
54 var mu sync.Mutex
55
56 var wg sync.WaitGroup
57 for i := 0; i < 100; i++ {
58 wg.Add(1)
59 go func(n int) {
60 defer wg.Done()
61 mu.Lock()
62 slice = append(slice, n) // Protected
63 mu.Unlock()
64 }(i)
65 }
66 wg.Wait()
67
68 fmt.Println("Slice length:", len(slice))
69}
70
71func main() {
72 fmt.Println("=== Race Condition Examples ===")
73
74 // mapRaceWrong() // Would cause race
75 mapRaceFixed()
76
77 // sliceRaceWrong() // Would cause race
78 sliceRaceFixed()
79}
💡 Key Takeaway: Go maps are not concurrent-safe! Always protect them with mutexes. The same goes for slices and any data structure that can be modified concurrently.
Closure Variable Capture
🎯 Practical Analogy: Capturing loop variables is like giving multiple people the same shopping list that keeps changing. Everyone ends up buying the same final item, not what was on the list when they started shopping.
💡 Key Takeaway: This is one of the most common Go beginner mistakes. Always pass loop variables as parameters to goroutines or create local copies.
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9// WRONG: Capturing loop variable
10func closureCaptureWrong() {
11 var wg sync.WaitGroup
12
13 for i := 0; i < 5; i++ {
14 wg.Add(1)
15 go func() {
16 defer wg.Done()
17 fmt.Println(i) // Captures loop variable
18 }()
19 }
20
21 wg.Wait()
22 // Prints unpredictable values, often all 5s
23}
24
25// CORRECT: Pass as parameter
26func closureCaptureFixed1() {
27 var wg sync.WaitGroup
28
29 for i := 0; i < 5; i++ {
30 wg.Add(1)
31 go func(n int) {
32 defer wg.Done()
33 fmt.Println(n) // Each goroutine gets its own copy
34 }(i)
35 }
36
37 wg.Wait()
38 // Prints 0, 1, 2, 3, 4 in some order
39}
40
41// CORRECT: Create local copy
42func closureCaptureFixed2() {
43 var wg sync.WaitGroup
44
45 for i := 0; i < 5; i++ {
46 i := i // Create local copy
47 wg.Add(1)
48 go func() {
49 defer wg.Done()
50 fmt.Println(i)
51 }()
52 }
53
54 wg.Wait()
55}
56
57func main() {
58 fmt.Println("Wrong:")
59 closureCaptureWrong()
60
61 fmt.Println("\nFixed:")
62 closureCaptureFixed1()
63
64 fmt.Println("\nFixed:")
65 closureCaptureFixed2()
66}
Real-world Example: In a web crawler, this bug caused all goroutines to process the same URL instead of distributing the work, severely limiting crawling efficiency.
Double-Checked Locking
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9type Resource struct {
10 data string
11}
12
13// WRONG: Double-checked locking without proper synchronization
14func wrongDoubleCheck() *Resource {
15 var resource *Resource
16 var mu sync.Mutex
17
18 initialize := func() *Resource {
19 mu.Lock()
20 defer mu.Unlock()
21
22 if resource == nil { // Check again under lock
23 resource = &Resource{data: "initialized"}
24 }
25
26 return resource
27 }
28
29 // First check without lock
30 if resource == nil {
31 return initialize()
32 }
33
34 return resource
35}
36
37// CORRECT: Use sync.Once
38func correctDoubleCheck() *Resource {
39 var resource *Resource
40 var once sync.Once
41
42 once.Do(func() {
43 resource = &Resource{data: "initialized"}
44 })
45
46 return resource
47}
48
49func main() {
50 var wg sync.WaitGroup
51
52 // Test with sync.Once
53 for i := 0; i < 10; i++ {
54 wg.Add(1)
55 go func() {
56 defer wg.Done()
57 res := correctDoubleCheck()
58 fmt.Printf("Got resource: %s\n", res.data)
59 }()
60 }
61
62 wg.Wait()
63}
Data Race Detection
Go provides a built-in race detector to find data races at runtime.
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9func demonstrateRaceDetector() {
10 // To detect races: go run -race main.go
11
12 var counter int
13 var wg sync.WaitGroup
14
15 // This will be detected by race detector
16 for i := 0; i < 10; i++ {
17 wg.Add(1)
18 go func() {
19 defer wg.Done()
20 counter++ // DATA RACE!
21 }()
22 }
23
24 wg.Wait()
25 fmt.Println("Counter:", counter)
26}
27
28func main() {
29 fmt.Println("=== Race Detector Demo ===")
30 fmt.Println("Run with: go run -race main.go")
31 demonstrateRaceDetector()
32}
How to Use the Race Detector:
1# Run with race detection
2go run -race main.go
3
4# Test with race detection
5go test -race ./...
6
7# Build with race detection
8go build -race
Race Detector Output Example:
==================
WARNING: DATA RACE
Write at 0x00c000018098 by goroutine 7:
main.main.func1()
/path/to/main.go:15 +0x3c
Previous write at 0x00c000018098 by goroutine 6:
main.main.func1()
/path/to/main.go:15 +0x3c
==================
Production Patterns
Message Passing
1// run
2package main
3
4import (
5 "fmt"
6 "time"
7)
8
9type Task struct {
10 ID int
11 Data string
12}
13
14type Result struct {
15 TaskID int
16 Output string
17}
18
19// Worker pool using channels
20func messagePassingPattern() {
21 tasks := make(chan Task, 10)
22 results := make(chan Result, 10)
23
24 // Start workers
25 for w := 1; w <= 3; w++ {
26 go worker(w, tasks, results)
27 }
28
29 // Send tasks
30 go func() {
31 for i := 1; i <= 5; i++ {
32 tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)}
33 }
34 close(tasks)
35 }()
36
37 // Collect results
38 for i := 1; i <= 5; i++ {
39 result := <-results
40 fmt.Printf("Result %d: %s\n", result.TaskID, result.Output)
41 }
42}
43
44func worker(id int, tasks <-chan Task, results chan<- Result) {
45 for task := range tasks {
46 fmt.Printf("Worker %d processing task %d\n", id, task.ID)
47 time.Sleep(100 * time.Millisecond)
48
49 results <- Result{
50 TaskID: task.ID,
51 Output: fmt.Sprintf("processed by worker %d", id),
52 }
53 }
54}
55
56func main() {
57 messagePassingPattern()
58}
Read-Copy-Update
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "sync/atomic"
8 "unsafe"
9)
10
11// RCU pattern for high-read, low-write scenarios
12type ConfigRCU struct {
13 current unsafe.Pointer // *Config
14}
15
16type Config struct {
17 Value string
18}
19
20func NewConfigRCU(initial *Config) *ConfigRCU {
21 return &ConfigRCU{
22 current: unsafe.Pointer(initial),
23 }
24}
25
26// Read
27func (c *ConfigRCU) Get() *Config {
28 return (*Config)(atomic.LoadPointer(&c.current))
29}
30
31// Write
32func (c *ConfigRCU) Update(newConfig *Config) {
33 atomic.StorePointer(&c.current, unsafe.Pointer(newConfig))
34}
35
36func main() {
37 config := NewConfigRCU(&Config{Value: "v1"})
38
39 var wg sync.WaitGroup
40
41 // Many readers
42 for i := 0; i < 10; i++ {
43 wg.Add(1)
44 go func(id int) {
45 defer wg.Done()
46 for j := 0; j < 100; j++ {
47 cfg := config.Get()
48 fmt.Printf("Reader %d: %s\n", id, cfg.Value)
49 }
50 }(i)
51 }
52
53 // One writer
54 wg.Add(1)
55 go func() {
56 defer wg.Done()
57 for i := 2; i <= 5; i++ {
58 config.Update(&Config{Value: fmt.Sprintf("v%d", i)})
59 }
60 }()
61
62 wg.Wait()
63}
Lock-Free Stack
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "sync/atomic"
8 "unsafe"
9)
10
11// Node in the stack
12type Node struct {
13 value int
14 next *Node
15}
16
17// LockFreeStack implements a lock-free stack using CAS
18type LockFreeStack struct {
19 head unsafe.Pointer // *Node
20}
21
22func NewLockFreeStack() *LockFreeStack {
23 return &LockFreeStack{}
24}
25
26// Push adds a value to the stack
27func (s *LockFreeStack) Push(value int) {
28 node := &Node{value: value}
29
30 for {
31 old := atomic.LoadPointer(&s.head)
32 node.next = (*Node)(old)
33
34 if atomic.CompareAndSwapPointer(&s.head, old, unsafe.Pointer(node)) {
35 return
36 }
37 }
38}
39
40// Pop removes and returns a value from the stack
41func (s *LockFreeStack) Pop() (int, bool) {
42 for {
43 old := atomic.LoadPointer(&s.head)
44 if old == nil {
45 return 0, false
46 }
47
48 node := (*Node)(old)
49 next := unsafe.Pointer(node.next)
50
51 if atomic.CompareAndSwapPointer(&s.head, old, next) {
52 return node.value, true
53 }
54 }
55}
56
57func main() {
58 stack := NewLockFreeStack()
59 var wg sync.WaitGroup
60
61 // Concurrent pushes
62 for i := 0; i < 10; i++ {
63 wg.Add(1)
64 go func(val int) {
65 defer wg.Done()
66 stack.Push(val)
67 fmt.Printf("Pushed: %d\n", val)
68 }(i)
69 }
70
71 wg.Wait()
72
73 // Concurrent pops
74 for i := 0; i < 10; i++ {
75 wg.Add(1)
76 go func() {
77 defer wg.Done()
78 if val, ok := stack.Pop(); ok {
79 fmt.Printf("Popped: %d\n", val)
80 }
81 }()
82 }
83
84 wg.Wait()
85}
Practice Exercises
Exercise 1: Fix the Race
🎯 Learning Objectives:
- Identify common data race patterns in concurrent Go code
- Master mutex and atomic operation solutions
- Understand when to use different synchronization primitives
- Practice race detection and debugging techniques
🌍 Real-World Context:
Data races are among the most dangerous and costly bugs in concurrent systems. A famous incident at Cloudflare in 2017 caused a global service outage when a race condition in their routing software sent traffic to wrong servers. Similarly, Uber experienced payment processing races that double-charged customers. Learning to spot and fix data races is essential for building reliable concurrent systems.
⏱️ Time Estimate: 30-45 minutes
📊 Difficulty: Intermediate
Find and fix the data race in the provided code. This classic counter race demonstrates how concurrent increments without proper synchronization can lead to incorrect results. Your solution should ensure thread safety while maintaining good performance.
1// run
2package main
3
4import "fmt"
5
6var counter int
7
8func main() {
9 for i := 0; i < 1000; i++ {
10 go func() {
11 counter++ // Race!
12 }()
13 }
14
15 fmt.Println(counter)
16}
Solution
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "sync/atomic"
8)
9
10// Solution 1: Using mutex
11func withMutex() {
12 var counter int
13 var mu sync.Mutex
14 var wg sync.WaitGroup
15
16 for i := 0; i < 1000; i++ {
17 wg.Add(1)
18 go func() {
19 defer wg.Done()
20 mu.Lock()
21 counter++
22 mu.Unlock()
23 }()
24 }
25
26 wg.Wait()
27 fmt.Println("With mutex:", counter)
28}
29
30// Solution 2: Using atomic
31func withAtomic() {
32 var counter atomic.Int64
33 var wg sync.WaitGroup
34
35 for i := 0; i < 1000; i++ {
36 wg.Add(1)
37 go func() {
38 defer wg.Done()
39 counter.Add(1)
40 }()
41 }
42
43 wg.Wait()
44 fmt.Println("With atomic:", counter.Load())
45}
46
47func main() {
48 withMutex()
49 withAtomic()
50}
Exercise 2: Implement a Thread-Safe Cache
🎯 Learning Objectives:
- Implement read-write mutex patterns for optimal performance
- Design thread-safe data structures with proper concurrency
- Understand RWMutex vs Mutex trade-offs
- Practice concurrent access patterns for read-heavy workloads
🌍 Real-World Context:
Thread-safe caches are fundamental building blocks in high-performance systems. Companies like Redis and MemSQL build entire products around concurrent caching patterns. In web services, caching reduces database load by 60-80% and improves response times dramatically. A well-designed concurrent cache can handle millions of requests per second while maintaining data consistency.
⏱️ Time Estimate: 45-60 minutes
📊 Difficulty: Intermediate
Create a thread-safe cache with Get and Set methods that optimizes for read-heavy workloads using RWMutex. Your implementation should handle concurrent reads without blocking while ensuring writes are safely synchronized, demonstrating the power of read-write locks for caching scenarios.
Solution
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7)
8
9type Cache struct {
10 mu sync.RWMutex
11 data map[string]interface{}
12}
13
14func NewCache() *Cache {
15 return &Cache{
16 data: make(map[string]interface{}),
17 }
18}
19
20func (c *Cache) Get(key string) (interface{}, bool) {
21 c.mu.RLock()
22 defer c.mu.RUnlock()
23 val, ok := c.data[key]
24 return val, ok
25}
26
27func (c *Cache) Set(key string, value interface{}) {
28 c.mu.Lock()
29 defer c.mu.Unlock()
30 c.data[key] = value
31}
32
33func main() {
34 cache := NewCache()
35 var wg sync.WaitGroup
36
37 // Writers
38 for i := 0; i < 10; i++ {
39 wg.Add(1)
40 go func(n int) {
41 defer wg.Done()
42 cache.Set(fmt.Sprintf("key%d", n), n)
43 }(i)
44 }
45
46 // Readers
47 for i := 0; i < 10; i++ {
48 wg.Add(1)
49 go func(n int) {
50 defer wg.Done()
51 if val, ok := cache.Get(fmt.Sprintf("key%d", n)); ok {
52 fmt.Printf("Got: %v\n", val)
53 }
54 }(i)
55 }
56
57 wg.Wait()
58}
Exercise 3: Happens-Before Detector
🎯 Learning Objectives:
- Deeply understand happens-before relationships in Go's memory model
- Visualize and demonstrate causal relationships between concurrent operations
- Master different synchronization primitives and their guarantees
- Learn to reason about memory ordering in concurrent systems
🌍 Real-World Context:
Understanding happens-before relationships is crucial for debugging complex concurrent systems. In distributed systems like Kubernetes, understanding causal relationships between events is essential for maintaining cluster state consistency. Companies building distributed databases spend significant engineering effort on ensuring proper happens-before guarantees to prevent data corruption and maintain consistency across nodes.
⏱️ Time Estimate: 60-90 minutes
📊 Difficulty: Advanced
Build a tool that demonstrates happens-before relationships between goroutines using different synchronization primitives. Your detector should visualize event ordering, prove that synchronization primitives establish proper happens-before guarantees, and help developers understand when memory writes become visible across goroutines.
Solution with Explanation
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "sync/atomic"
8 "time"
9)
10
11type Event struct {
12 id int
13 timestamp time.Time
14 goroutine int
15 message string
16}
17
18type HappensBeforeDetector struct {
19 events []Event
20 mu sync.Mutex
21 nextID atomic.Int32
22}
23
24func NewDetector() *HappensBeforeDetector {
25 return &HappensBeforeDetector{
26 events: make([]Event, 0, 100),
27 }
28}
29
30func (d *HappensBeforeDetector) Record(goroutineID int, message string) {
31 event := Event{
32 id: int(d.nextID.Add(1)),
33 timestamp: time.Now(),
34 goroutine: goroutineID,
35 message: message,
36 }
37
38 d.mu.Lock()
39 d.events = append(d.events, event)
40 d.mu.Unlock()
41}
42
43func (d *HappensBeforeDetector) Report() {
44 d.mu.Lock()
45 defer d.mu.Unlock()
46
47 fmt.Println("\n=== Happens-Before Event Timeline ===")
48 for _, e := range d.events {
49 fmt.Printf("[G%d] Event %d: %s @ %s\n",
50 e.goroutine, e.id, e.message, e.timestamp.Format("15:04:05.000"))
51 }
52}
53
54// Scenario 1: Channel happens-before
55func demonstrateChannelHB(d *HappensBeforeDetector) {
56 fmt.Println("\n--- Scenario 1: Channel Happens-Before ---")
57
58 ch := make(chan int)
59 var sharedData int
60
61 go func() {
62 d.Record(1, "G1: Writing sharedData = 42")
63 sharedData = 42
64 d.Record(1, "G1: Sending on channel")
65 ch <- 1 // Send happens-before receive completes
66 d.Record(1, "G1: Send completed")
67 }()
68
69 go func() {
70 d.Record(2, "G2: Waiting on channel")
71 <-ch // Receive
72 d.Record(2, "G2: Received from channel")
73 // Guaranteed to see sharedData = 42
74 d.Record(2, fmt.Sprintf("G2: Reading sharedData = %d", sharedData))
75 }()
76
77 time.Sleep(100 * time.Millisecond)
78}
79
80// Scenario 2: Mutex happens-before
81func demonstrateMutexHB(d *HappensBeforeDetector) {
82 fmt.Println("\n--- Scenario 2: Mutex Happens-Before ---")
83
84 var mu sync.Mutex
85 var counter int
86
87 go func() {
88 d.Record(3, "G3: Acquiring lock")
89 mu.Lock()
90 d.Record(3, "G3: Lock acquired")
91 counter = 100
92 d.Record(3, "G3: Set counter = 100")
93 mu.Unlock()
94 d.Record(3, "G3: Lock released")
95 }()
96
97 time.Sleep(50 * time.Millisecond)
98
99 go func() {
100 d.Record(4, "G4: Acquiring lock")
101 mu.Lock()
102 d.Record(4, "G4: Lock acquired")
103 d.Record(4, fmt.Sprintf("G4: Read counter = %d", counter))
104 mu.Unlock()
105 d.Record(4, "G4: Lock released")
106 }()
107
108 time.Sleep(100 * time.Millisecond)
109}
110
111// Scenario 3: WaitGroup happens-before
112func demonstrateWaitGroupHB(d *HappensBeforeDetector) {
113 fmt.Println("\n--- Scenario 3: WaitGroup Happens-Before ---")
114
115 var wg sync.WaitGroup
116 var results []int
117
118 for i := 1; i <= 3; i++ {
119 wg.Add(1)
120 go func(id int) {
121 defer wg.Done()
122 d.Record(id+4, fmt.Sprintf("G%d: Computing result", id+4))
123 time.Sleep(10 * time.Millisecond)
124 results = append(results, id*10)
125 d.Record(id+4, fmt.Sprintf("G%d: Done() called", id+4))
126 }(i)
127 }
128
129 d.Record(8, "G8: Calling Wait()")
130 wg.Wait()
131 d.Record(8, "G8: Wait() returned")
132 d.Record(8, fmt.Sprintf("G8: All results collected: %v", results))
133}
134
135func main() {
136 detector := NewDetector()
137
138 detector.Record(0, "Main: Starting happens-before detection")
139
140 demonstrateChannelHB(detector)
141 demonstrateMutexHB(detector)
142 demonstrateWaitGroupHB(detector)
143
144 time.Sleep(200 * time.Millisecond)
145 detector.Report()
146
147 fmt.Println("\n=== Happens-Before Guarantees ===")
148 fmt.Println("1. Channel send happens-before receive completes")
149 fmt.Println("2. Mutex unlock happens-before subsequent lock")
150 fmt.Println("3. WaitGroup Done() happens-before Wait() returns")
151 fmt.Println("4. Goroutine creation happens-before goroutine execution")
152}
Explanation:
This detector demonstrates happens-before relationships:
-
Channel Synchronization: Write to
sharedDatahappens-before send on channel, which happens-before receive completes. Therefore, receive is guaranteed to see the write. -
Mutex Synchronization: Write to
counterinside critical section, unlock happens-before the next Lock. Second goroutine guaranteed to see the write. -
WaitGroup Synchronization: All
Done()calls happen-beforeWait()returns. Main goroutine guaranteed to see all writes from workers. -
Event Recording: Timestamp-based timeline shows ordering and demonstrates causal relationships between events.
Key Insight: Happens-before guarantees enable reasoning about concurrent code correctness without worrying about low-level memory ordering details.
Exercise 4: Race Condition Finder
🎯 Learning Objectives:
- Master Go's race detector for identifying data races
- Learn to recognize common race condition patterns
- Practice systematic debugging of concurrent code
- Understand the difference between symptom and root cause in concurrent bugs
🌍 Real-World Context:
Race conditions are notoriously difficult to debug because they're often intermittent and timing-dependent. Companies like Google have entire teams dedicated to race detection tools. The Go race detector has prevented countless production incidents by catching races early in development. Understanding how to use it effectively is a critical skill for any Go developer working on concurrent systems.
⏱️ Time Estimate: 75-90 minutes
📊 Difficulty: Advanced
Create a program that intentionally contains data races, then fix them using proper synchronization. Your race finder should demonstrate common race patterns, show how the Go race detector identifies them, and provide systematic fixes. This exercise teaches you to think like a race detector and prevent subtle bugs before they reach production.
Solution with Explanation
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "sync/atomic"
8 "time"
9)
10
11// Scenario 1: Map race
12type BrokenCache struct {
13 data map[string]int
14}
15
16func (c *BrokenCache) IncreaseCount(key string) {
17 c.data[key]++
18}
19
20// Scenario 1: Map race FIXED
21type SafeCache struct {
22 mu sync.RWMutex
23 data map[string]int
24}
25
26func NewSafeCache() *SafeCache {
27 return &SafeCache{
28 data: make(map[string]int),
29 }
30}
31
32func (c *SafeCache) IncreaseCount(key string) {
33 c.mu.Lock()
34 c.data[key]++
35 c.mu.Unlock()
36}
37
38func (c *SafeCache) Get(key string) int {
39 c.mu.RLock()
40 defer c.mu.RUnlock()
41 return c.data[key]
42}
43
44// Scenario 2: Counter race
45type BrokenCounter struct {
46 value int
47}
48
49func (c *BrokenCounter) Increment() {
50 c.value++
51}
52
53// Scenario 2: Counter race FIXED
54type SafeCounter struct {
55 value atomic.Int64
56}
57
58func (c *SafeCounter) Increment() {
59 c.value.Add(1)
60}
61
62func (c *SafeCounter) Value() int64 {
63 return c.value.Load()
64}
65
66// Scenario 3: Closure capture FIXED
67func safeClosureCapture() []int {
68 var mu sync.Mutex
69 var results []int
70 var wg sync.WaitGroup
71
72 for i := 0; i < 5; i++ {
73 wg.Add(1)
74 go func(n int) {
75 defer wg.Done()
76 mu.Lock()
77 results = append(results, n)
78 mu.Unlock()
79 }(i)
80 }
81
82 wg.Wait()
83 return results
84}
85
86// Race detector helper
87func detectRaces(name string, fn func()) {
88 fmt.Printf("\n=== %s ===\n", name)
89 fmt.Println("Running test...")
90
91 done := make(chan bool)
92 go func() {
93 fn()
94 done <- true
95 }()
96
97 select {
98 case <-done:
99 fmt.Println("Test completed")
100 case <-time.After(time.Second):
101 fmt.Println("Test timeout")
102 }
103}
104
105func main() {
106 fmt.Println("=== Race Condition Finder ===")
107 fmt.Println("Run with: go run -race main.go")
108 fmt.Println("(The race detector will report data races in BROKEN versions)\n")
109
110 // Test safe cache
111 detectRaces("FIXED: Safe Map", func() {
112 cache := NewSafeCache()
113 var wg sync.WaitGroup
114
115 for i := 0; i < 10; i++ {
116 wg.Add(1)
117 go func() {
118 defer wg.Done()
119 cache.IncreaseCount("key")
120 _ = cache.Get("key")
121 }()
122 }
123 wg.Wait()
124 fmt.Printf("Final count: %d\n", cache.Get("key"))
125 })
126
127 // Test safe counter
128 detectRaces("FIXED: Safe Counter", func() {
129 counter := &SafeCounter{}
130 var wg sync.WaitGroup
131
132 for i := 0; i < 100; i++ {
133 wg.Add(1)
134 go func() {
135 defer wg.Done()
136 counter.Increment()
137 }()
138 }
139 wg.Wait()
140 fmt.Printf("Final value: %d\n", counter.Value())
141 })
142
143 // Test closure capture
144 fmt.Println("\n=== FIXED: Safe Closure ===")
145 results := safeClosureCapture()
146 fmt.Printf("Results: %v\n", results)
147
148 fmt.Println("\n=== Summary ===")
149 fmt.Println("Common Race Conditions:")
150 fmt.Println("1. Concurrent map access → Use sync.RWMutex")
151 fmt.Println("2. Counter increment → Use atomic operations")
152 fmt.Println("3. Closure variable capture → Pass as parameter")
153 fmt.Println("4. Slice append → Use mutex protection")
154 fmt.Println("\nDetection: Run tests with `go test -race`")
155}
Explanation:
This exercise demonstrates common race conditions and their fixes:
-
Map Race: Concurrent reads/writes to map cause crashes. Fix with
sync.RWMutex. -
Counter Race:
counter++is read-modify-write. Fix withatomic.Int64. -
Closure Capture: All goroutines capture same loop variable. Fix by passing value as parameter.
-
Race Detector: Use
go run -raceto detect data races at runtime.
Production Impact: Data races cause subtle bugs. Always test with -race flag during development.
Exercise 5: Channel Synchronization
🎯 Learning Objectives:
- Master producer-consumer patterns using Go channels
- Understand the synchronization guarantees of different channel types
- Learn to design concurrent pipelines with proper backpressure
- Practice channel-based coordination and shutdown patterns
🌍 Real-World Context:
Producer-consumer pipelines are the backbone of many production systems. Companies like Netflix use channel-based pipelines for video processing, while data streaming platforms like Kafka implement similar patterns at massive scale. Understanding channel synchronization is essential for building high-throughput systems that can gracefully handle backpressure and shutdown cleanly without data loss.
⏱️ Time Estimate: 90-120 minutes
📊 Difficulty: Expert
Implement a producer-consumer pipeline using channels with proper synchronization, demonstrating the happens-before guarantees of different channel types. Your pipeline should handle concurrent producers and consumers, implement graceful shutdown, show the differences between buffered and unbuffered channels, and demonstrate proper coordination patterns used in production systems.
Solution with Explanation
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "time"
8)
9
10type Task struct {
11 ID int
12 Data string
13}
14
15type Result struct {
16 TaskID int
17 Output string
18 Worker int
19}
20
21type Pipeline struct {
22 tasks chan Task
23 results chan Result
24 done chan struct{}
25}
26
27func NewPipeline(bufferSize int) *Pipeline {
28 return &Pipeline{
29 tasks: make(chan Task, bufferSize),
30 results: make(chan Result, bufferSize),
31 done: make(chan struct{}),
32 }
33}
34
35func (p *Pipeline) Produce(count int) {
36 fmt.Printf("Producer: Starting to produce %d tasks\n", count)
37
38 for i := 1; i <= count; i++ {
39 task := Task{
40 ID: i,
41 Data: fmt.Sprintf("task-%d", i),
42 }
43
44 fmt.Printf("Producer: Sending task %d\n", task.ID)
45 p.tasks <- task
46
47 time.Sleep(50 * time.Millisecond)
48 }
49
50 close(p.tasks)
51 fmt.Println("Producer: All tasks sent, channel closed")
52}
53
54func (p *Pipeline) Consume(workerID int, wg *sync.WaitGroup) {
55 defer wg.Done()
56
57 fmt.Printf("Worker %d: Started\n", workerID)
58
59 for task := range p.tasks {
60 fmt.Printf("Worker %d: Processing task %d\n", workerID, task.ID)
61
62 time.Sleep(100 * time.Millisecond)
63
64 result := Result{
65 TaskID: task.ID,
66 Output: fmt.Sprintf("processed-%s", task.Data),
67 Worker: workerID,
68 }
69
70 p.results <- result
71 fmt.Printf("Worker %d: Completed task %d\n", workerID, task.ID)
72 }
73
74 fmt.Printf("Worker %d: No more tasks, exiting\n", workerID)
75}
76
77func (p *Pipeline) Collect(expected int) []Result {
78 fmt.Println("Collector: Started")
79
80 var results []Result
81
82 for i := 0; i < expected; i++ {
83 result := <-p.results
84 results = append(results, result)
85 fmt.Printf("Collector: Received result from task %d (worker %d)\n",
86 result.TaskID, result.Worker)
87 }
88
89 close(p.done)
90 fmt.Println("Collector: All results collected")
91
92 return results
93}
94
95func demonstrateChannelTypes() {
96 fmt.Println("\n=== Unbuffered vs Buffered Channels ===\n")
97
98 fmt.Println("--- Unbuffered Channel ---")
99 unbuffered := make(chan int)
100
101 go func() {
102 fmt.Println("Sender: Sending on unbuffered channel...")
103 unbuffered <- 42
104 fmt.Println("Sender: Send completed")
105 }()
106
107 time.Sleep(100 * time.Millisecond)
108 fmt.Println("Receiver: Receiving from unbuffered channel...")
109 val := <-unbuffered
110 fmt.Printf("Receiver: Got %d\n", val)
111
112 time.Sleep(100 * time.Millisecond)
113
114 fmt.Println("\n--- Buffered Channel ---")
115 buffered := make(chan int, 3)
116
117 go func() {
118 for i := 1; i <= 3; i++ {
119 fmt.Printf("Sender: Sending %d\n", i)
120 buffered <- i
121 fmt.Printf("Sender: Sent %d\n", i)
122 }
123
124 fmt.Println("Sender: Sending 4th item...")
125 buffered <- 4
126 fmt.Println("Sender: Sent 4th item")
127 }()
128
129 time.Sleep(100 * time.Millisecond)
130
131 for i := 1; i <= 4; i++ {
132 val := <-buffered
133 fmt.Printf("Receiver: Got %d\n", val)
134 time.Sleep(50 * time.Millisecond)
135 }
136}
137
138func main() {
139 fmt.Println("=== Channel Synchronization Pipeline ===\n")
140
141 pipeline := NewPipeline(5)
142
143 const numTasks = 10
144 const numWorkers = 3
145
146 var wg sync.WaitGroup
147 for i := 1; i <= numWorkers; i++ {
148 wg.Add(1)
149 go pipeline.Consume(i, &wg)
150 }
151
152 resultsChan := make(chan []Result)
153 go func() {
154 results := pipeline.Collect(numTasks)
155 resultsChan <- results
156 }()
157
158 go pipeline.Produce(numTasks)
159
160 wg.Wait()
161 fmt.Println("\nAll workers finished")
162
163 results := <-resultsChan
164 <-pipeline.done
165
166 fmt.Printf("\n=== Pipeline Complete ===\n")
167 fmt.Printf("Total tasks processed: %d\n", len(results))
168
169 workerCounts := make(map[int]int)
170 for _, r := range results {
171 workerCounts[r.Worker]++
172 }
173
174 fmt.Println("\nWorker distribution:")
175 for worker, count := range workerCounts {
176 fmt.Printf(" Worker %d: %d tasks\n", worker, count)
177 }
178
179 demonstrateChannelTypes()
180
181 fmt.Println("\n=== Happens-Before Guarantees ===")
182 fmt.Println("1. Producer send happens-before consumer receive")
183 fmt.Println("2. Consumer send happens-before collector receive")
184 fmt.Println("3. Channel close happens-before range loop exits")
185 fmt.Println("4. All worker Done() happens-before wg.Wait() returns")
186}
Explanation:
This pipeline demonstrates channel synchronization:
-
Producer-Consumer Pattern: Producer sends tasks, workers process them, collector gathers results with clean shutdown via channel close.
-
Happens-Before with Channels: Send happens-before receive completes, guaranteeing visibility of all writes. No data races despite concurrent access.
-
Unbuffered Channels: Send blocks until receive occurs, providing synchronization point where sender knows receiver has received.
-
Buffered Channels: Send doesn't block until buffer full, still provides happens-before guarantees, better throughput for producer/consumer.
-
Coordination: WaitGroup for worker completion, channel close for signaling, multiple synchronization primitives working together.
Production Use: This pattern is used for parallel processing with guaranteed correctness.
Exercise 6: Memory Barrier Visualizer
🎯 Learning Objectives:
- Understand memory barriers and their effects on ordering
- Visualize reordering allowed by weak memory models
- Master the use of atomic operations for proper synchronization
- Learn to reason about cross-goroutine visibility
🌍 Real-World Context:
Memory barriers are fundamental to understanding how modern processors optimize memory access. In high-performance systems like databases and trading platforms, understanding when and why memory barriers are needed prevents subtle bugs. For example, a missing memory barrier in a database transaction system could allow transactions to appear out of order, violating ACID properties and corrupting data.
⏱️ Time Estimate: 90-120 minutes
📊 Difficulty: Expert
Build a tool that demonstrates memory reordering and the effects of memory barriers. Your visualizer should show how operations can be reordered without synchronization, demonstrate the guarantees provided by different synchronization primitives, and prove that proper synchronization establishes correct memory ordering.
Solution with Explanation
1// run
2package main
3
4import (
5 "fmt"
6 "runtime"
7 "sync"
8 "sync/atomic"
9 "time"
10)
11
12type MemoryBarrierTest struct {
13 name string
14 iterations int
15 reorderings int
16}
17
18// Test 1: No synchronization (may observe reordering)
19func testNoSync(iterations int) *MemoryBarrierTest {
20 test := &MemoryBarrierTest{
21 name: "No Synchronization",
22 iterations: iterations,
23 }
24
25 for i := 0; i < iterations; i++ {
26 var x, y, a, b int
27
28 done := make(chan bool, 2)
29
30 go func() {
31 x = 1
32 a = y
33 done <- true
34 }()
35
36 go func() {
37 y = 1
38 b = x
39 done <- true
40 }()
41
42 <-done
43 <-done
44
45 // a=0, b=0 shows reordering!
46 if a == 0 && b == 0 {
47 test.reorderings++
48 }
49 }
50
51 return test
52}
53
54// Test 2: With atomic operations (no reordering)
55func testAtomic(iterations int) *MemoryBarrierTest {
56 test := &MemoryBarrierTest{
57 name: "Atomic Operations",
58 iterations: iterations,
59 }
60
61 for i := 0; i < iterations; i++ {
62 var x, y atomic.Int32
63 var a, b int32
64
65 done := make(chan bool, 2)
66
67 go func() {
68 x.Store(1)
69 a = y.Load()
70 done <- true
71 }()
72
73 go func() {
74 y.Store(1)
75 b = x.Load()
76 done <- true
77 }()
78
79 <-done
80 <-done
81
82 // With atomic, a=0 and b=0 should NOT occur
83 if a == 0 && b == 0 {
84 test.reorderings++
85 }
86 }
87
88 return test
89}
90
91// Test 3: With mutex (no reordering)
92func testMutex(iterations int) *MemoryBarrierTest {
93 test := &MemoryBarrierTest{
94 name: "Mutex Synchronization",
95 iterations: iterations,
96 }
97
98 for i := 0; i < iterations; i++ {
99 var x, y, a, b int
100 var mu sync.Mutex
101
102 done := make(chan bool, 2)
103
104 go func() {
105 mu.Lock()
106 x = 1
107 mu.Unlock()
108
109 mu.Lock()
110 a = y
111 mu.Unlock()
112
113 done <- true
114 }()
115
116 go func() {
117 mu.Lock()
118 y = 1
119 mu.Unlock()
120
121 mu.Lock()
122 b = x
123 mu.Unlock()
124
125 done <- true
126 }()
127
128 <-done
129 <-done
130
131 // With mutex, a=0 and b=0 should NOT occur
132 if a == 0 && b == 0 {
133 test.reorderings++
134 }
135 }
136
137 return test
138}
139
140// Test 4: With channels (no reordering)
141func testChannels(iterations int) *MemoryBarrierTest {
142 test := &MemoryBarrierTest{
143 name: "Channel Synchronization",
144 iterations: iterations,
145 }
146
147 for i := 0; i < iterations; i++ {
148 var x, y, a, b int
149 ch1 := make(chan struct{})
150 ch2 := make(chan struct{})
151
152 done := make(chan bool, 2)
153
154 go func() {
155 x = 1
156 close(ch1) // Signal
157 <-ch2 // Wait
158 a = y
159 done <- true
160 }()
161
162 go func() {
163 y = 1
164 close(ch2) // Signal
165 <-ch1 // Wait
166 b = x
167 done <- true
168 }()
169
170 <-done
171 <-done
172
173 // With channels, a=0 and b=0 should NOT occur
174 if a == 0 && b == 0 {
175 test.reorderings++
176 }
177 }
178
179 return test
180}
181
182func main() {
183 fmt.Println("=== Memory Barrier Visualizer ===\n")
184 fmt.Println("Testing memory ordering with different synchronization primitives")
185 fmt.Println("Looking for reordering: a=0, b=0 (impossible with sequential consistency)\n")
186
187 runtime.GOMAXPROCS(runtime.NumCPU())
188
189 iterations := 10000
190
191 // Run tests
192 tests := []*MemoryBarrierTest{
193 testNoSync(iterations),
194 testAtomic(iterations),
195 testMutex(iterations),
196 testChannels(iterations),
197 }
198
199 // Print results
200 fmt.Println("=== Results ===\n")
201 for _, test := range tests {
202 percentage := float64(test.reorderings) / float64(test.iterations) * 100
203 fmt.Printf("%s:\n", test.name)
204 fmt.Printf(" Iterations: %d\n", test.iterations)
205 fmt.Printf(" Reorderings observed: %d (%.2f%%)\n", test.reorderings, percentage)
206
207 if test.reorderings > 0 {
208 fmt.Printf(" ⚠️ Weak memory ordering detected!\n")
209 } else {
210 fmt.Printf(" ✓ Proper synchronization: No reordering\n")
211 }
212 fmt.Println()
213 }
214
215 // Explanation
216 fmt.Println("=== Explanation ===\n")
217 fmt.Println("1. No Synchronization:")
218 fmt.Println(" - Compiler/CPU can reorder operations")
219 fmt.Println(" - a=0, b=0 is possible due to reordering")
220 fmt.Println(" - This is a data race!\n")
221
222 fmt.Println("2. Atomic Operations:")
223 fmt.Println(" - Provide sequential consistency for atomic variables")
224 fmt.Println(" - Prevent reordering across atomic operations")
225 fmt.Println(" - a=0, b=0 cannot occur\n")
226
227 fmt.Println("3. Mutex Synchronization:")
228 fmt.Println(" - Lock provides acquire semantics")
229 fmt.Println(" - Unlock provides release semantics")
230 fmt.Println(" - Establishes happens-before relationships\n")
231
232 fmt.Println("4. Channel Synchronization:")
233 fmt.Println(" - Send/receive operations are synchronization points")
234 fmt.Println(" - Provide happens-before guarantees")
235 fmt.Println(" - Ensure proper memory ordering\n")
236
237 fmt.Println("=== Memory Barrier Types ===\n")
238 fmt.Println("1. Compiler Barrier: Prevents compiler reordering")
239 fmt.Println("2. CPU Barrier: Prevents hardware reordering")
240 fmt.Println("3. Full Barrier: Both compiler + CPU barriers")
241 fmt.Println(" (Go's sync primitives provide full barriers)\n")
242}
Explanation:
This visualizer demonstrates memory reordering:
-
No Synchronization: Shows that without proper synchronization, operations can be reordered, leading to impossible outcomes in a sequentially consistent model.
-
Atomic Operations: Demonstrate that atomic operations prevent reordering and provide proper memory ordering.
-
Mutex Synchronization: Shows that mutex lock/unlock operations establish proper happens-before relationships.
-
Channel Synchronization: Demonstrates that channel operations provide memory ordering guarantees.
Key Insights:
- Weak memory models allow reordering for performance
- Synchronization primitives provide memory barriers
- Memory barriers ensure proper ordering and visibility
- Always use synchronization to prevent data races
Exercise 7: Lock-Free Queue Implementation
🎯 Learning Objectives:
- Implement lock-free data structures using Compare-And-Swap
- Master atomic operations for concurrent algorithms
- Understand ABA problem and prevention techniques
- Learn trade-offs between lock-based and lock-free algorithms
🌍 Real-World Context:
Lock-free data structures are critical for high-performance systems where lock contention becomes a bottleneck. Companies like Microsoft use lock-free queues in their Windows kernel. Intel CPUs provide hardware support for atomic operations that make lock-free algorithms possible. Understanding lock-free programming is essential for building systems that need to handle millions of operations per second with minimal latency.
⏱️ Time Estimate: 120-150 minutes
📊 Difficulty: Expert
Implement a lock-free queue using atomic Compare-And-Swap operations. Your queue should support concurrent enqueue and dequeue operations without locks, handle the ABA problem correctly, provide linearizability guarantees, and demonstrate performance benefits over lock-based implementations.
Solution with Explanation
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "sync/atomic"
8 "time"
9 "unsafe"
10)
11
12// Node in the lock-free queue
13type Node struct {
14 value interface{}
15 next unsafe.Pointer // *Node
16}
17
18// LockFreeQueue implements a lock-free queue using CAS
19type LockFreeQueue struct {
20 head unsafe.Pointer // *Node
21 tail unsafe.Pointer // *Node
22}
23
24func NewLockFreeQueue() *LockFreeQueue {
25 // Create dummy node
26 dummy := &Node{}
27 q := &LockFreeQueue{
28 head: unsafe.Pointer(dummy),
29 tail: unsafe.Pointer(dummy),
30 }
31 return q
32}
33
34// Enqueue adds a value to the queue
35func (q *LockFreeQueue) Enqueue(value interface{}) {
36 node := &Node{value: value}
37
38 for {
39 // Load tail and next
40 tail := (*Node)(atomic.LoadPointer(&q.tail))
41 next := atomic.LoadPointer(&tail.next)
42
43 // Check if tail is still consistent
44 if tail == (*Node)(atomic.LoadPointer(&q.tail)) {
45 // Is tail pointing to the last node?
46 if next == nil {
47 // Try to link node at the end of the list
48 if atomic.CompareAndSwapPointer(&tail.next, next, unsafe.Pointer(node)) {
49 // Enqueue is done, try to swing tail to the inserted node
50 atomic.CompareAndSwapPointer(&q.tail, unsafe.Pointer(tail), unsafe.Pointer(node))
51 return
52 }
53 } else {
54 // Tail was not pointing to last node, try to swing tail to next
55 atomic.CompareAndSwapPointer(&q.tail, unsafe.Pointer(tail), next)
56 }
57 }
58 }
59}
60
61// Dequeue removes and returns a value from the queue
62func (q *LockFreeQueue) Dequeue() (interface{}, bool) {
63 for {
64 // Load head, tail, and next
65 head := (*Node)(atomic.LoadPointer(&q.head))
66 tail := (*Node)(atomic.LoadPointer(&q.tail))
67 next := (*Node)(atomic.LoadPointer(&head.next))
68
69 // Check if head is still consistent
70 if head == (*Node)(atomic.LoadPointer(&q.head)) {
71 // Is queue empty or tail falling behind?
72 if head == tail {
73 // Is queue empty?
74 if next == nil {
75 return nil, false
76 }
77 // Tail is falling behind, try to advance it
78 atomic.CompareAndSwapPointer(&q.tail, unsafe.Pointer(tail), unsafe.Pointer(next))
79 } else {
80 // Read value before CAS, otherwise another dequeue might free the next node
81 value := next.value
82
83 // Try to swing head to the next node
84 if atomic.CompareAndSwapPointer(&q.head, unsafe.Pointer(head), unsafe.Pointer(next)) {
85 return value, true
86 }
87 }
88 }
89 }
90}
91
92// IsEmpty checks if the queue is empty
93func (q *LockFreeQueue) IsEmpty() bool {
94 head := (*Node)(atomic.LoadPointer(&q.head))
95 next := atomic.LoadPointer(&head.next)
96 return next == nil
97}
98
99// Lock-based queue for comparison
100type LockQueue struct {
101 mu sync.Mutex
102 items []interface{}
103}
104
105func NewLockQueue() *LockQueue {
106 return &LockQueue{
107 items: make([]interface{}, 0),
108 }
109}
110
111func (q *LockQueue) Enqueue(value interface{}) {
112 q.mu.Lock()
113 q.items = append(q.items, value)
114 q.mu.Unlock()
115}
116
117func (q *LockQueue) Dequeue() (interface{}, bool) {
118 q.mu.Lock()
119 defer q.mu.Unlock()
120
121 if len(q.items) == 0 {
122 return nil, false
123 }
124
125 value := q.items[0]
126 q.items = q.items[1:]
127 return value, true
128}
129
130// Benchmark helper
131func benchmarkQueue(name string, enqueue func(interface{}), dequeue func() (interface{}, bool)) {
132 start := time.Now()
133
134 const operations = 100000
135 const goroutines = 10
136
137 var wg sync.WaitGroup
138
139 // Producers
140 for i := 0; i < goroutines; i++ {
141 wg.Add(1)
142 go func(id int) {
143 defer wg.Done()
144 for j := 0; j < operations/goroutines; j++ {
145 enqueue(id*1000 + j)
146 }
147 }(i)
148 }
149
150 // Consumers
151 for i := 0; i < goroutines; i++ {
152 wg.Add(1)
153 go func() {
154 defer wg.Done()
155 for j := 0; j < operations/goroutines; j++ {
156 for {
157 if _, ok := dequeue(); ok {
158 break
159 }
160 time.Sleep(time.Microsecond)
161 }
162 }
163 }()
164 }
165
166 wg.Wait()
167
168 duration := time.Since(start)
169 fmt.Printf("%s:\n", name)
170 fmt.Printf(" Operations: %d\n", operations*2)
171 fmt.Printf(" Duration: %v\n", duration)
172 fmt.Printf(" Ops/sec: %.0f\n\n", float64(operations*2)/duration.Seconds())
173}
174
175func main() {
176 fmt.Println("=== Lock-Free Queue Implementation ===\n")
177
178 // Test correctness
179 fmt.Println("Testing correctness:")
180 lfq := NewLockFreeQueue()
181
182 // Enqueue
183 for i := 1; i <= 5; i++ {
184 lfq.Enqueue(i)
185 fmt.Printf("Enqueued: %d\n", i)
186 }
187
188 // Dequeue
189 fmt.Println("\nDequeuing:")
190 for !lfq.IsEmpty() {
191 if val, ok := lfq.Dequeue(); ok {
192 fmt.Printf("Dequeued: %d\n", val)
193 }
194 }
195
196 // Test with concurrent operations
197 fmt.Println("\n=== Concurrent Operations Test ===\n")
198
199 lfq = NewLockFreeQueue()
200 var wg sync.WaitGroup
201
202 // Multiple producers
203 for i := 0; i < 3; i++ {
204 wg.Add(1)
205 go func(id int) {
206 defer wg.Done()
207 for j := 0; j < 10; j++ {
208 lfq.Enqueue(id*100 + j)
209 }
210 }(i)
211 }
212
213 // Multiple consumers
214 results := make([]interface{}, 0, 30)
215 var mu sync.Mutex
216
217 for i := 0; i < 3; i++ {
218 wg.Add(1)
219 go func() {
220 defer wg.Done()
221 for j := 0; j < 10; j++ {
222 for {
223 if val, ok := lfq.Dequeue(); ok {
224 mu.Lock()
225 results = append(results, val)
226 mu.Unlock()
227 break
228 }
229 time.Sleep(time.Microsecond)
230 }
231 }
232 }()
233 }
234
235 wg.Wait()
236
237 fmt.Printf("Enqueued: 30 items\n")
238 fmt.Printf("Dequeued: %d items\n", len(results))
239 fmt.Printf("All items processed: %v\n\n", len(results) == 30)
240
241 // Performance comparison
242 fmt.Println("=== Performance Comparison ===\n")
243
244 // Lock-free queue
245 lfq = NewLockFreeQueue()
246 benchmarkQueue("Lock-Free Queue", lfq.Enqueue, lfq.Dequeue)
247
248 // Lock-based queue
249 lq := NewLockQueue()
250 benchmarkQueue("Lock-Based Queue", lq.Enqueue, lq.Dequeue)
251
252 // Explanation
253 fmt.Println("=== Key Concepts ===\n")
254 fmt.Println("1. Compare-And-Swap (CAS):")
255 fmt.Println(" - Atomic operation that updates value only if it matches expected")
256 fmt.Println(" - Provides lock-free synchronization")
257 fmt.Println(" - Retry loop handles contention\n")
258
259 fmt.Println("2. Lock-Free Progress:")
260 fmt.Println(" - At least one thread makes progress")
261 fmt.Println(" - No thread can block others indefinitely")
262 fmt.Println(" - Better scalability under high contention\n")
263
264 fmt.Println("3. Memory Ordering:")
265 fmt.Println(" - Atomic operations provide memory barriers")
266 fmt.Println(" - Ensures visibility across threads")
267 fmt.Println(" - No data races despite lack of locks\n")
268
269 fmt.Println("4. ABA Problem:")
270 fmt.Println(" - Value changes from A to B and back to A")
271 fmt.Println(" - CAS thinks nothing changed")
272 fmt.Println(" - Solution: Version numbers or hazard pointers\n")
273}
Explanation:
This lock-free queue implementation demonstrates:
-
CAS Operations: Uses Compare-And-Swap for lock-free synchronization. Multiple threads can attempt operations simultaneously, and CAS ensures only one succeeds atomically.
-
Michael-Scott Algorithm: Industry-standard lock-free queue algorithm that uses a dummy node to simplify edge cases.
-
Memory Ordering: Atomic operations provide necessary memory barriers to ensure proper visibility across threads.
-
Performance: Lock-free algorithms often outperform lock-based ones under high contention because they avoid lock overhead and allow more parallelism.
Advantages:
- No lock contention
- Better scalability
- No deadlock possible
- Predictable latency
Disadvantages:
- More complex to implement correctly
- Retry loops consume CPU
- May have lower throughput under low contention
- Requires atomic operation support
Exercise 8: Deadlock Detector
🎯 Learning Objectives:
- Implement deadlock detection algorithms for concurrent programs
- Master cycle detection in wait-for graphs
- Learn to identify circular dependencies in lock acquisition
- Practice systematic debugging of deadlock scenarios
🌍 Real-World Context:
Deadlocks are among the most challenging bugs to detect and fix in concurrent systems. In database systems, deadlock detection is essential for ensuring progress. Commercial databases like PostgreSQL and MySQL implement sophisticated deadlock detection algorithms. Understanding deadlock detection helps you design systems that avoid deadlocks or handle them gracefully when they occur.
⏱️ Time Estimate: 120-150 minutes
📊 Difficulty: Expert
Build a deadlock detector that monitors lock acquisition patterns and detects potential deadlocks in concurrent programs. Your detector should track lock acquisitions, build a wait-for graph, detect cycles indicating deadlocks, and provide actionable feedback on how to resolve the deadlocks.
Solution with Explanation
1// run
2package main
3
4import (
5 "fmt"
6 "sync"
7 "time"
8)
9
10// LockID uniquely identifies a lock
11type LockID int
12
13// GoroutineID uniquely identifies a goroutine
14type GoroutineID int
15
16// LockEvent represents a lock operation
17type LockEvent struct {
18 Goroutine GoroutineID
19 Lock LockID
20 Operation string // "acquire", "release", "waiting"
21 Timestamp time.Time
22}
23
24// DeadlockDetector monitors lock operations
25type DeadlockDetector struct {
26 mu sync.Mutex
27
28 // Current lock holders: lockID -> goroutineID
29 holders map[LockID]GoroutineID
30
31 // Waiting goroutines: goroutineID -> lockID
32 waiting map[GoroutineID]LockID
33
34 // Events history
35 events []LockEvent
36
37 // Detected deadlocks
38 deadlocks [][]GoroutineID
39}
40
41func NewDeadlockDetector() *DeadlockDetector {
42 return &DeadlockDetector{
43 holders: make(map[LockID]GoroutineID),
44 waiting: make(map[GoroutineID]LockID),
45 events: make([]LockEvent, 0),
46 deadlocks: make([][]GoroutineID, 0),
47 }
48}
49
50// RecordAcquire records a successful lock acquisition
51func (dd *DeadlockDetector) RecordAcquire(goroutine GoroutineID, lock LockID) {
52 dd.mu.Lock()
53 defer dd.mu.Unlock()
54
55 dd.holders[lock] = goroutine
56 delete(dd.waiting, goroutine)
57
58 dd.events = append(dd.events, LockEvent{
59 Goroutine: goroutine,
60 Lock: lock,
61 Operation: "acquire",
62 Timestamp: time.Now(),
63 })
64}
65
66// RecordRelease records a lock release
67func (dd *DeadlockDetector) RecordRelease(goroutine GoroutineID, lock LockID) {
68 dd.mu.Lock()
69 defer dd.mu.Unlock()
70
71 delete(dd.holders, lock)
72
73 dd.events = append(dd.events, LockEvent{
74 Goroutine: goroutine,
75 Lock: lock,
76 Operation: "release",
77 Timestamp: time.Now(),
78 })
79}
80
81// RecordWaiting records a goroutine waiting for a lock
82func (dd *DeadlockDetector) RecordWaiting(goroutine GoroutineID, lock LockID) {
83 dd.mu.Lock()
84 defer dd.mu.Unlock()
85
86 dd.waiting[goroutine] = lock
87
88 dd.events = append(dd.events, LockEvent{
89 Goroutine: goroutine,
90 Lock: lock,
91 Operation: "waiting",
92 Timestamp: time.Now(),
93 })
94
95 // Check for deadlock
96 if cycle := dd.detectCycle(); len(cycle) > 0 {
97 dd.deadlocks = append(dd.deadlocks, cycle)
98 fmt.Printf("\n⚠️ DEADLOCK DETECTED: %v\n\n", cycle)
99 }
100}
101
102// detectCycle detects cycles in the wait-for graph
103func (dd *DeadlockDetector) detectCycle() []GoroutineID {
104 // Build wait-for graph: goroutine -> goroutine
105 graph := make(map[GoroutineID]GoroutineID)
106
107 for waitingG, wantedLock := range dd.waiting {
108 if holder, ok := dd.holders[wantedLock]; ok {
109 graph[waitingG] = holder
110 }
111 }
112
113 // Detect cycle using DFS
114 visited := make(map[GoroutineID]bool)
115 recStack := make(map[GoroutineID]bool)
116 path := make([]GoroutineID, 0)
117
118 var dfs func(GoroutineID) []GoroutineID
119 dfs = func(g GoroutineID) []GoroutineID {
120 visited[g] = true
121 recStack[g] = true
122 path = append(path, g)
123
124 if next, ok := graph[g]; ok {
125 if !visited[next] {
126 if cycle := dfs(next); cycle != nil {
127 return cycle
128 }
129 } else if recStack[next] {
130 // Found cycle
131 cycleStart := 0
132 for i, v := range path {
133 if v == next {
134 cycleStart = i
135 break
136 }
137 }
138 return path[cycleStart:]
139 }
140 }
141
142 recStack[g] = false
143 path = path[:len(path)-1]
144 return nil
145 }
146
147 for g := range dd.waiting {
148 if !visited[g] {
149 if cycle := dfs(g); cycle != nil {
150 return cycle
151 }
152 }
153 }
154
155 return nil
156}
157
158// Report generates a deadlock report
159func (dd *DeadlockDetector) Report() {
160 dd.mu.Lock()
161 defer dd.mu.Unlock()
162
163 fmt.Println("\n=== Deadlock Detection Report ===\n")
164
165 if len(dd.deadlocks) == 0 {
166 fmt.Println("No deadlocks detected ✓")
167 return
168 }
169
170 fmt.Printf("Detected %d deadlock(s):\n\n", len(dd.deadlocks))
171
172 for i, cycle := range dd.deadlocks {
173 fmt.Printf("Deadlock %d:\n", i+1)
174 fmt.Printf(" Cycle: %v\n", cycle)
175
176 // Show what each goroutine is waiting for
177 for _, g := range cycle {
178 if lock, ok := dd.waiting[g]; ok {
179 holder := dd.holders[lock]
180 fmt.Printf(" G%d waiting for Lock%d (held by G%d)\n", g, lock, holder)
181 }
182 }
183 fmt.Println()
184 }
185
186 // Show event timeline
187 fmt.Println("Event Timeline:")
188 for _, event := range dd.events {
189 fmt.Printf(" [%s] G%d %s Lock%d\n",
190 event.Timestamp.Format("15:04:05.000"),
191 event.Goroutine, event.Operation, event.Lock)
192 }
193}
194
195// MonitoredMutex wraps sync.Mutex with deadlock detection
196type MonitoredMutex struct {
197 id LockID
198 mu sync.Mutex
199 detector *DeadlockDetector
200}
201
202func NewMonitoredMutex(id LockID, detector *DeadlockDetector) *MonitoredMutex {
203 return &MonitoredMutex{
204 id: id,
205 detector: detector,
206 }
207}
208
209func (m *MonitoredMutex) Lock(goroutine GoroutineID) {
210 m.detector.RecordWaiting(goroutine, m.id)
211 m.mu.Lock()
212 m.detector.RecordAcquire(goroutine, m.id)
213}
214
215func (m *MonitoredMutex) Unlock(goroutine GoroutineID) {
216 m.mu.Unlock()
217 m.detector.RecordRelease(goroutine, m.id)
218}
219
220func main() {
221 fmt.Println("=== Deadlock Detector ===\n")
222
223 detector := NewDeadlockDetector()
224
225 // Create two locks
226 lock1 := NewMonitoredMutex(1, detector)
227 lock2 := NewMonitoredMutex(2, detector)
228
229 // Scenario 1: No deadlock (correct lock ordering)
230 fmt.Println("Scenario 1: Correct Lock Ordering (No Deadlock)")
231 var wg sync.WaitGroup
232
233 wg.Add(2)
234
235 // Goroutine 1: Lock1 -> Lock2
236 go func() {
237 defer wg.Done()
238 lock1.Lock(1)
239 time.Sleep(100 * time.Millisecond)
240 lock2.Lock(1)
241 fmt.Println("G1: Got both locks")
242 lock2.Unlock(1)
243 lock1.Unlock(1)
244 }()
245
246 // Goroutine 2: Lock1 -> Lock2 (same order)
247 go func() {
248 defer wg.Done()
249 time.Sleep(50 * time.Millisecond)
250 lock1.Lock(2)
251 lock2.Lock(2)
252 fmt.Println("G2: Got both locks")
253 lock2.Unlock(2)
254 lock1.Unlock(2)
255 }()
256
257 wg.Wait()
258 time.Sleep(200 * time.Millisecond)
259
260 detector.Report()
261
262 // Scenario 2: Deadlock (incorrect lock ordering)
263 fmt.Println("\n\nScenario 2: Incorrect Lock Ordering (Deadlock)")
264
265 detector = NewDeadlockDetector()
266 lock1 = NewMonitoredMutex(1, detector)
267 lock2 = NewMonitoredMutex(2, detector)
268
269 wg.Add(2)
270
271 // Goroutine 3: Lock1 -> Lock2
272 go func() {
273 defer wg.Done()
274 lock1.Lock(3)
275 fmt.Println("G3: Acquired Lock1")
276 time.Sleep(100 * time.Millisecond)
277
278 fmt.Println("G3: Trying to acquire Lock2...")
279 lock2.Lock(3)
280 fmt.Println("G3: Acquired Lock2")
281
282 lock2.Unlock(3)
283 lock1.Unlock(3)
284 }()
285
286 // Goroutine 4: Lock2 -> Lock1 (reverse order!)
287 go func() {
288 defer wg.Done()
289 time.Sleep(50 * time.Millisecond)
290
291 lock2.Lock(4)
292 fmt.Println("G4: Acquired Lock2")
293 time.Sleep(100 * time.Millisecond)
294
295 fmt.Println("G4: Trying to acquire Lock1...")
296 lock1.Lock(4)
297 fmt.Println("G4: Acquired Lock1")
298
299 lock1.Unlock(4)
300 lock2.Unlock(4)
301 }()
302
303 // Give time for deadlock to form
304 time.Sleep(500 * time.Millisecond)
305
306 detector.Report()
307
308 fmt.Println("\n=== Deadlock Prevention Strategies ===\n")
309 fmt.Println("1. Lock Ordering:")
310 fmt.Println(" - Always acquire locks in the same order")
311 fmt.Println(" - Assign global ordering to all locks")
312 fmt.Println(" - Example: Always Lock1 before Lock2\n")
313
314 fmt.Println("2. Lock Timeout:")
315 fmt.Println(" - Use TryLock with timeout")
316 fmt.Println(" - Release all locks if can't acquire")
317 fmt.Println(" - Retry with backoff\n")
318
319 fmt.Println("3. Lock Hierarchy:")
320 fmt.Println(" - Define levels for locks")
321 fmt.Println(" - Only acquire locks at higher levels")
322 fmt.Println(" - Prevents circular dependencies\n")
323
324 fmt.Println("4. Deadlock Detection:")
325 fmt.Println(" - Monitor lock acquisitions")
326 fmt.Println(" - Build wait-for graph")
327 fmt.Println(" - Detect cycles")
328 fmt.Println(" - Abort one transaction to break cycle\n")
329}
Explanation:
This deadlock detector demonstrates:
-
Wait-For Graph: Tracks which goroutines are waiting for locks held by other goroutines. A deadlock exists when there's a cycle in this graph.
-
Cycle Detection: Uses depth-first search (DFS) to detect cycles in the wait-for graph. A cycle means goroutines are waiting in a circular pattern.
-
Event Timeline: Records all lock operations with timestamps, enabling post-mortem analysis of deadlocks.
-
Deadlock Prevention: The example shows how proper lock ordering prevents deadlocks, while incorrect ordering causes them.
Key Insights:
Coffman Conditions (all must be true for deadlock):
- Mutual Exclusion: Resources can't be shared
- Hold and Wait: Hold locks while waiting for others
- No Preemption: Locks can't be forcibly taken
- Circular Wait: Cycle in wait-for graph
Prevention Strategies:
- Lock Ordering: Break circular wait
- Timeouts: Break hold and wait
- Try-Lock: Allow backoff and retry
- Lock Hierarchy: Enforce ordering discipline
Summary
💡 Key Takeaway: The Go memory model is not about theory—it's about writing correct concurrent programs that work reliably in production. Understanding happens-before relationships is essential for preventing subtle bugs that can cost millions.
The Go memory model ensures predictable concurrent behavior:
- Happens-Before: Defines when memory operations are visible across goroutines
- Synchronization: Use mutexes, channels, or atomics for safe concurrent access
- Memory Ordering: Go doesn't guarantee sequential consistency without synchronization
- Race Prevention: Understand common patterns and use
-racedetector - Production Patterns: Message passing, RCU, and proper synchronization
When to Use What: Synchronization Cheat Sheet
🎯 Practical Guide: Choose the right tool for your concurrent programming needs:
| Scenario | Best Choice | Why |
|---|---|---|
| Simple counter | sync/atomic |
Fastest for single variables, no locks |
| Complex data structures | sync.Mutex |
Protects multiple variables as a unit |
| Read-heavy operations | sync.RWMutex |
Multiple readers, single writer optimization |
| Coordination between goroutines | Channels | Go's preferred approach, both communication and sync |
| One-time initialization | sync.Once |
Guaranteed single execution, thread-safe |
| Wait for multiple goroutines | sync.WaitGroup |
Clean shutdown patterns |
| Condition variables | sync.Cond |
Wait for specific conditions |
| Lock-free algorithms | atomic.Value |
High-performance, no contention |
Common Pitfalls to Avoid
⚠️ Important: These patterns cause production bugs:
- Maps without protection - Always use mutexes with concurrent map access
- Loop variable capture - Pass values as parameters to goroutines
- Assuming sequential consistency - Use synchronization, not assumptions
- Forgetting the race detector - Run tests with
go test -race - Incorrect lock ordering - Always acquire locks in the same order
- Missing synchronization - Don't assume writes are visible without synchronization
Real-world Impact: These patterns have caused outages at major companies including Cloudflare, Uber, and others. The cost of a single data race can range from thousands to millions of dollars.
Best Practices Checklist
- ✅ Always run with race detector in development:
go run -race - ✅ Prefer channels for goroutine communication: Go's philosophy
- ✅ Use mutexes for protecting shared state: Simple and effective
- ✅ Consider atomics for simple counters: Better performance
- ✅ Never share mutable state without synchronization: Root cause of races
- ✅ Test under realistic load: Races often appear only under stress
- ✅ Use proper lock ordering: Prevent deadlocks
- ✅ Document synchronization patterns: Help future maintainers
Production Tip: Set up CI pipelines that run with -race enabled. It's better to catch races in development than in production at 3 AM.
Memory Model Rules:
- Within a goroutine: program order
- Channel send happens-before receive completes
- Mutex unlock happens-before subsequent lock
- sync.Once.Do() completes before any Do() returns
- Goroutine creation happens-before goroutine execution
- Channel close happens-before receive of zero value
- For buffered channels, Kth send happens-before (K+C)th receive completes
- sync.WaitGroup.Done() happens-before corresponding Wait() returns
Final Thought: Understanding the memory model transforms concurrent programming from guesswork into engineering. You're not just writing code—you're specifying exactly when and how different parts of your program will see each other's changes.