Implement the dining philosophers problem in Go without deadlock or starvation. Compare resource ordering with an arbiter.
The naive version has every philosopher take the left fork, then the right. If all five grab their left fork at the same time you get a circular wait, which is a deadlock. There are two standard ways to fix it.
1. Resource ordering: number the forks and always lock the lower number first. This breaks the circular-wait condition, because one philosopher (the last one) ends up reaching "right" first.
func Dine(n, meals int) {
forks := make([]sync.Mutex, n)
seats := make(chan struct{}, n-1) // used only by the arbiter variant
_ = seats
var wg sync.WaitGroup
for id := range n {
wg.Add(1)
go func() {
defer wg.Done()
a, b := id, (id+1)%n
if a > b {
a, b = b, a // global order: lower index first
}
for m := range meals {
// arbiter variant: seats <- struct{}{}
forks[a].Lock()
forks[b].Lock()
fmt.Printf("philosopher %d eats meal %d\n", id, m)
forks[b].Unlock()
forks[a].Unlock()
// arbiter variant: <-seats
runtime.Gosched() // "think"
}
}()
}
wg.Wait()
}
2. Arbiter (waiter): a semaphore with n-1 seats means at most n-1 philosophers compete at once, so at least one of them can always get both forks. A stricter arbiter is a single goroutine that grants both forks atomically on request, which also lets it enforce fairness with a FIFO queue.
Comparison: ordering needs no coordinator and is fully decentralized, but it requires a global order on resources, which is hard when locks are acquired dynamically. The semaphore arbiter is simple and order-free but limits concurrency to n-1. The central arbiter is a serialization point and a potential bottleneck, but it is the only one that can guarantee fairness.
Starvation: sync.Mutex switches to starvation mode when a waiter has been blocked for more than 1ms, and then hands the lock off FIFO. That limits starvation in practice but it is not a formal fairness guarantee. A buffered-channel semaphore is also roughly FIFO for blocked senders. Mention the four Coffman conditions (mutual exclusion, hold-and-wait, no preemption, circular wait): ordering breaks circular wait, and the arbiter breaks hold-and-wait.
More on Classic Concurrency Coding Problems
- Q517Implement ping-pong between two goroutines that stops cleanly after N rounds or on context cancellation.
- Q518Print "foo", "bar", "baz" in strict order from three goroutines, repeated N times.
- Q520Bank transfer: two goroutines transfer money between accounts A->B and B->A with per-account mutexes. Why does it deadlock, and how does consistent lock ordering fix it?
- Q521Implement a bounded blocking queue (producer-consumer) using sync.Cond, then using channels. Compare the two.
- Q522Implement a reusable barrier (CyclicBarrier) that N goroutines wait on before moving to the next phase.
- Q523Implement a Future/Promise type in Go with Get(ctx) and error propagation using generics.