The Santa Claus Problem
The Santa Claus problem is a delightful synchronization challenge by J.A. Trono (1994). It demonstrates group coordination, prioritization, and conditional wake-up patterns.
The Scenario
Characters:
- Santa Claus (1)
- Reindeer (9)
- Elves (10)
The rules:
- Santa sleeps in his shop at the North Pole
- Santa can be awakened by either:
- All 9 reindeer return from vacation → prepare sleigh, deliver toys
- Any 3 elves have problems → get help from Santa
- Reindeer have priority over elves
- Santa helps 3 elves at a time, then goes back to sleep
- After delivering toys, Santa unhitches reindeer and goes back to sleep
The Challenge
- Coordination: Wake Santa only when conditions met
- Grouping: Exactly 9 reindeer OR 3 elves
- Priority: Reindeer interrupt elf help
- Fairness: All elves should eventually get help
Real-World Applications
This pattern models many production scenarios:
- Batch processing: Process when batch size reached or timeout
- Database commits: Group transactions for efficiency
- Network packet batching: Send when buffer full or timeout
- Thread pool work stealing: Coordinate worker groups
- Consensus protocols: Wait for quorum
Implementation in Go
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type SantaWorkshop struct {
// Reindeer coordination
reindeerWaiting int
reindeerMu sync.Mutex
reindeerReady *sync.Cond
// Elf coordination
elfWaiting int
elfGroup []int // IDs of elves in current group
elfMu sync.Mutex
elfReady *sync.Cond
elfGetHelp *sync.Cond
// Santa coordination
santaSleeping bool
santaMu sync.Mutex
santaWakeup chan string // "reindeer" or "elves"
santaDone chan struct{}
// Statistics
deliveries int
elfSessions int
elvesSolved map[int]int
statsmu sync.Mutex
}
func NewSantaWorkshop() *SantaWorkshop {
sw := &SantaWorkshop{
santaWakeup: make(chan string, 1),
santaDone: make(chan struct{}),
elvesSolved: make(map[int]int),
santaSleeping: true,
}
sw.reindeerReady = sync.NewCond(&sw.reindeerMu)
sw.elfReady = sync.NewCond(&sw.elfMu)
sw.elfGetHelp = sync.NewCond(&sw.elfMu)
return sw
}
// Santa's main loop
func (sw *SantaWorkshop) Santa() {
for {
fmt.Println("\n[Santa] 😴 Sleeping...")
// Wait to be woken up
reason := <-sw.santaWakeup
sw.santaMu.Lock()
sw.santaSleeping = false
sw.santaMu.Unlock()
switch reason {
case "reindeer":
fmt.Println("[Santa] 🎅 Woken by reindeer! Preparing sleigh...")
sw.deliverToys()
case "elves":
fmt.Println("[Santa] 🎅 Woken by elves! Helping with problems...")
sw.helpElves()
}
sw.santaMu.Lock()
sw.santaSleeping = true
sw.santaMu.Unlock()
// Check if simulation is done
select {
case <-sw.santaDone:
fmt.Println("\n[Santa] 🎄 Merry Christmas! Going on vacation...")
return
default:
}
}
}
func (sw *SantaWorkshop) deliverToys() {
fmt.Println("[Santa] 🦌 Hitching up all 9 reindeer...")
time.Sleep(200 * time.Millisecond)
fmt.Println("[Santa] 🎁 Delivering toys around the world!")
time.Sleep(500 * time.Millisecond)
fmt.Println("[Santa] ✓ Toys delivered! Unhitching reindeer...")
sw.statsmu.Lock()
sw.deliveries++
sw.statsmu.Unlock()
// Release reindeer
sw.reindeerMu.Lock()
sw.reindeerWaiting = 0
sw.reindeerReady.Broadcast()
sw.reindeerMu.Unlock()
}
func (sw *SantaWorkshop) helpElves() {
sw.elfMu.Lock()
elves := make([]int, len(sw.elfGroup))
copy(elves, sw.elfGroup)
sw.elfMu.Unlock()
fmt.Printf("[Santa] 🧝 Helping elves %v with their problems...\n", elves)
time.Sleep(300 * time.Millisecond)
fmt.Printf("[Santa] ✓ Helped elves %v!\n", elves)
sw.statsmu.Lock()
sw.elfSessions++
for _, elfID := range elves {
sw.elvesSolved[elfID]++
}
sw.statsmu.Unlock()
// Release elves
sw.elfMu.Lock()
sw.elfGroup = nil
sw.elfWaiting = 0
sw.elfGetHelp.Broadcast()
sw.elfMu.Unlock()
}
// Reindeer return from vacation
func (sw *SantaWorkshop) Reindeer(id int) {
fmt.Printf("[Reindeer %d] 🦌 Returning from vacation...\n", id)
sw.reindeerMu.Lock()
sw.reindeerWaiting++
if sw.reindeerWaiting == 9 {
// All reindeer ready! Wake Santa
fmt.Println("[Reindeer] 🔔 All 9 reindeer ready! Waking Santa...")
// Priority: interrupt elves if needed
select {
case sw.santaWakeup <- "reindeer":
default:
}
// Wait for Santa to finish
for sw.reindeerWaiting > 0 {
sw.reindeerReady.Wait()
}
sw.reindeerMu.Unlock()
fmt.Printf("[Reindeer %d] ✓ Back on vacation!\n", id)
} else {
fmt.Printf("[Reindeer %d] Waiting (%d/9 here)\n", id, sw.reindeerWaiting)
sw.reindeerMu.Unlock()
}
}
// Elf needs help
func (sw *SantaWorkshop) Elf(id int, problems int) {
for i := 0; i < problems; i++ {
// Random delay before needing help
time.Sleep(time.Duration(200+rand.Intn(300)) * time.Millisecond)
fmt.Printf("[Elf %d] 🧝 Need help with problem %d!\n", id, i+1)
sw.elfMu.Lock()
// Wait if group is full or Santa is busy
for len(sw.elfGroup) >= 3 {
sw.elfReady.Wait()
}
// Join the group
sw.elfGroup = append(sw.elfGroup, id)
sw.elfWaiting++
currentGroupSize := len(sw.elfGroup)
fmt.Printf("[Elf %d] Joined group (%d/3)\n", id, currentGroupSize)
if currentGroupSize == 3 {
// Group full! Wake Santa
fmt.Println("[Elves] 🔔 3 elves ready! Waking Santa...")
sw.santaMu.Lock()
sleeping := sw.santaSleeping
sw.santaMu.Unlock()
if sleeping {
sw.santaWakeup <- "elves"
}
}
// Wait for Santa to help
for sw.elfWaiting > 0 {
sw.elfGetHelp.Wait()
}
sw.elfMu.Unlock()
fmt.Printf("[Elf %d] ✓ Got help from Santa!\n", id)
}
fmt.Printf("[Elf %d] 🎉 All problems solved!\n", id)
}
func main() {
fmt.Println("=== Santa Claus Problem ===")
fmt.Println("Coordinating elves and reindeer\n")
workshop := NewSantaWorkshop()
var wg sync.WaitGroup
// Start Santa
wg.Add(1)
go func() {
defer wg.Done()
workshop.Santa()
}()
// Start elves (each has 2-3 problems)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
problems := 2 + rand.Intn(2)
workshop.Elf(id, problems)
}(i)
}
// Start reindeer (return twice for 2 deliveries)
for delivery := 0; delivery < 2; delivery++ {
time.Sleep(time.Duration(2+delivery*3) * time.Second)
for i := 0; i < 9; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
workshop.Reindeer(id)
}(i)
}
}
// Wait for all elves to finish
time.Sleep(8 * time.Second)
close(workshop.santaDone)
wg.Wait()
// Print statistics
workshop.statsmu.Lock()
fmt.Println("\n📊 Workshop Statistics:")
fmt.Printf(" Toy deliveries: %d\n", workshop.deliveries)
fmt.Printf(" Elf help sessions: %d\n", workshop.elfSessions)
fmt.Println(" Problems solved per elf:")
for id := 0; id < 10; id++ {
fmt.Printf(" Elf %d: %d problems\n", id, workshop.elvesSolved[id])
}
workshop.statsmu.Unlock()
fmt.Println("\n🎄 Merry Christmas! 🎄")
}
Key Synchronization Patterns
1. Group Formation
elfGroup = append(elfGroup, id)
if len(elfGroup) == 3 {
// Wake Santa
}
Accumulate until threshold reached.
2. Priority Handling
// Reindeer always take priority
select {
case santaWakeup <- "reindeer":
default:
}
Reindeer can interrupt elf help.
3. Barrier Synchronization
// All wait for Santa to finish
for reindeerWaiting > 0 {
reindeerReady.Wait()
}
Classic barrier pattern.
Alternative: Channel-Based Solution
Here’s a more Go-idiomatic approach using channels:
package main
import (
"fmt"
"time"
)
type ReindeerRequest struct {
id int
done chan struct{}
}
type ElfRequest struct {
id int
done chan struct{}
}
type ChannelWorkshop struct {
reindeerQueue chan ReindeerRequest
elfQueue chan ElfRequest
shutdown chan struct{}
}
func NewChannelWorkshop() *ChannelWorkshop {
return &ChannelWorkshop{
reindeerQueue: make(chan ReindeerRequest, 9),
elfQueue: make(chan ElfRequest, 10),
shutdown: make(chan struct{}),
}
}
func (cw *ChannelWorkshop) Santa() {
var reindeerBatch []ReindeerRequest
var elfBatch []ElfRequest
for {
select {
case <-cw.shutdown:
return
case req := <-cw.reindeerQueue:
reindeerBatch = append(reindeerBatch, req)
if len(reindeerBatch) == 9 {
fmt.Println("[Santa] 🎅 All reindeer ready! Delivering toys...")
time.Sleep(500 * time.Millisecond)
for _, r := range reindeerBatch {
close(r.done)
}
reindeerBatch = nil
}
case req := <-cw.elfQueue:
elfBatch = append(elfBatch, req)
if len(elfBatch) == 3 {
fmt.Printf("[Santa] 🎅 Helping elves %d, %d, %d\n",
elfBatch[0].id, elfBatch[1].id, elfBatch[2].id)
time.Sleep(300 * time.Millisecond)
for _, e := range elfBatch {
close(e.done)
}
elfBatch = nil
}
}
}
}
func (cw *ChannelWorkshop) Reindeer(id int) {
done := make(chan struct{})
cw.reindeerQueue <- ReindeerRequest{id: id, done: done}
<-done
}
func (cw *ChannelWorkshop) Elf(id int) {
done := make(chan struct{})
cw.elfQueue <- ElfRequest{id: id, done: done}
<-done
}
Real-World Example: Batch Processing System
package main
import (
"fmt"
"sync"
"time"
)
type Transaction struct {
ID int
Amount float64
Done chan struct{}
}
type BatchProcessor struct {
transactions []Transaction
batchSize int
timeout time.Duration
mu sync.Mutex
cond *sync.Cond
processed int
}
func NewBatchProcessor(batchSize int, timeout time.Duration) *BatchProcessor {
bp := &BatchProcessor{
batchSize: batchSize,
timeout: timeout,
}
bp.cond = sync.NewCond(&bp.mu)
return bp
}
// Processor waits for batch or timeout
func (bp *BatchProcessor) Processor() {
for {
bp.mu.Lock()
// Wait for batch size or timeout
timer := time.AfterFunc(bp.timeout, func() {
bp.mu.Lock()
if len(bp.transactions) > 0 {
bp.cond.Signal()
}
bp.mu.Unlock()
})
for len(bp.transactions) < bp.batchSize {
bp.cond.Wait()
// If woken up, check if we should process
if len(bp.transactions) >= bp.batchSize {
timer.Stop()
break
}
}
// Process batch
batch := bp.transactions
bp.transactions = nil
bp.mu.Unlock()
if len(batch) > 0 {
bp.processBatch(batch)
}
}
}
func (bp *BatchProcessor) processBatch(batch []Transaction) {
fmt.Printf("[Processor] Processing batch of %d transactions\n", len(batch))
time.Sleep(200 * time.Millisecond)
total := 0.0
for _, txn := range batch {
total += txn.Amount
close(txn.Done)
}
bp.mu.Lock()
bp.processed += len(batch)
bp.mu.Unlock()
fmt.Printf("[Processor] ✓ Batch complete: $%.2f total\n", total)
}
// Submit adds transaction to batch
func (bp *BatchProcessor) Submit(txn Transaction) {
bp.mu.Lock()
defer bp.mu.Unlock()
bp.transactions = append(bp.transactions, txn)
if len(bp.transactions) >= bp.batchSize {
bp.cond.Signal()
}
}
Advantages of This Pattern
✓ Efficient batching - Process groups together ✓ Priority handling - Critical tasks can interrupt ✓ Resource optimization - Santa helps multiple elves at once ✓ Clear coordination - Explicit group formation
When to Use
✓ Use when:
- Operations are more efficient in batches
- Need to coordinate groups
- Have priority levels
- Can wait for threshold or timeout
✗ Simpler solutions when:
- Single-item processing is fine
- No batching benefits
- No priority needed
- Immediate processing required
Try It Yourself
- Add timeouts - Elves give up if waiting too long
- Variable thresholds - 2-4 elves instead of exactly 3
- Add priorities - Urgent elf problems
- Track fairness - Ensure all elves get equal help
- Add cancellation - Elves can leave the queue
This is part 12 of “Golang Experiments: Classic Concurrency Problems”