Implement a reusable barrier (CyclicBarrier) that N goroutines wait on before moving to the next phase.
The hard part is making the barrier reusable. A fast goroutine can finish phase k and call Wait for phase k+1 before a slow waiter from phase k has even woken up. If waiters check count, the fast goroutine's increment corrupts it and the slow goroutine sleeps forever. The fix is a generation counter: each waiter remembers the generation it arrived in and waits until that generation changes.
type Barrier struct {
mu sync.Mutex
cond *sync.Cond
n int
count int
gen uint64
}
func NewBarrier(n int) *Barrier {
b := &Barrier{n: n}
b.cond = sync.NewCond(&b.mu)
return b
}
// Wait blocks until n goroutines have called Wait for the current phase.
// It reports true for exactly one goroutine per phase (the last arriver),
// which is handy for running a per-phase action.
func (b *Barrier) Wait() bool {
b.mu.Lock()
defer b.mu.Unlock()
gen := b.gen
b.count++
if b.count == b.n {
b.count = 0
b.gen++ // open the barrier and start the next generation
b.cond.Broadcast()
return true
}
for gen == b.gen {
b.cond.Wait()
}
return false
}
Channel alternative: keep a release chan struct{} for each generation. The last arriver calls close(release) and swaps in a fresh channel while holding the mutex. The other goroutines grab the current channel under the lock and then wait with select { case <-ch: case <-ctx.Done(): }. That makes waits cancellable, but a cancelled waiter leaves count wrong. Java handles this by marking the barrier broken and failing every waiter, and you have to do something similar.
Why not sync.WaitGroup? A WaitGroup is one-way: the workers call Done and a different goroutine calls Wait. Reusing it needs another Add before anyone calls Wait again. Calling Add while a Wait is still in progress is a documented misuse, and it races across phases. What the interviewer is looking for: spotting the phase-overlap race and fixing it with a generation number, plus a plan for cancellation (a broken-barrier state).
More on Classic Concurrency Coding Problems
- 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.
- Q523Implement a Future/Promise type in Go with Get(ctx) and error propagation using generics.
- Q524Implement a concurrent prime sieve with a daisy chain of goroutines. How many goroutines does it create, and how do you stop it without leaks?
- Q525Concurrently crawl URLs (the Tour of Go web crawler) with depth limits, deduplication and bounded parallelism.
- Q526Count word frequencies in a large set of files concurrently and return the top K words. How do you merge per-worker maps efficiently?