Concurrency Roadmap/Classic Concurrency Interview Patterns

Ordered & Alternating Execution (Print-in-Order, Odd-Even)

Force threads into a strict sequence or a strict round-robin alternation using one-shot gates or a shared turn variable — and know exactly why busy-spinning or sleep-based ordering is a red flag, not a shortcut.

!!!3/5Theory: 40m4 problems
Language-specific mechanics: Concurrency Language Manual

The two flavors of this pattern

"Make these threads run in a specific order" shows up in interviews in exactly two shapes, and it pays to name them separately because the fix is slightly different for each:

  • Strict ordering — thread A's work must complete before thread B's work starts, which must complete before thread C's starts, and so on. Once the order is established it never repeats (Print in Order: first()second()third(), each called exactly once).
  • Strict alternation — two or more threads must take turns repeatedly, in a fixed round-robin pattern, for many iterations (FooBar: foo, bar, foo, bar, ...; Print Zero Even Odd: zero, odd, zero, even, zero, odd, ...).

Both are really the same underlying question — "how do I make one thread wait for a signal that only another specific thread can send?" — just applied once versus applied in a loop. Every primitive from the earlier topics on this roadmap (locks, semaphores, condition variables) can express this signal; the pattern is choosing the simplest one that matches the shape of the constraint.

Why "just poll a flag" is wrong — and a red flag to an interviewer

The instinctive first attempt is almost always a shared boolean or counter that one thread busy-spins on:

# thread B, trying to wait for thread A while not a_is_done: pass # or Thread.sleep(1) do_second_thing()

This is wrong on two levels, and interviewers listen for whether you catch both:

  1. Correctness, if the flag isn't synchronized properly. Without a volatile/atomic read or a memory barrier, a compiler or CPU is entitled to cache a_is_done in a register and never observe the other thread's write — the loop can spin forever in principle, not just in the worst-case-latency sense.
  2. Even if made correct with volatile/atomics, busy-spinning is a design smell. It burns a full CPU core doing nothing useful, it doesn't scale past a handful of waiting threads, and — the detail candidates miss most often — inserting Thread.sleep() calls to "fix" ordering issues is not a fix at all. Sleep-based ordering happens to pass on your machine because your thread scheduler is fast and lightly loaded; it is not a correctness argument, and a stress-tested or heavily-loaded grader will flip the interleaving and fail it. Saying this out loud — "sleeping to enforce order doesn't give a correctness guarantee, only a probabilistic one" — is one of the highest-signal sentences you can say in this style of interview.

The fix in both cases is the same: block the waiting thread on a real synchronization primitive so the OS scheduler wakes it exactly when the signal arrives, with zero spinning and a proper happens-before relationship guaranteed by the primitive itself.

Strict ordering: one-shot gates

For "A before B before C," the cleanest tool is one gate per ordering constraint — a binary semaphore initialized to 0 (locked) or a latch, one per boundary you need to enforce:

gate_AB = semaphore(0) # locked until A signals it gate_BC = semaphore(0) # locked until B signals it # thread A run_first() release(gate_AB) # unlock B # thread B acquire(gate_AB) # blocks until A releases it run_second() release(gate_BC) # unlock C # thread C acquire(gate_BC) # blocks until B releases it run_third()

Each gate is used exactly once — acquired once, released once — which is precisely what a CountDownLatch/one-shot event is for, and is arguably a clearer expression of intent than a semaphore here (a latch's API doesn't let you accidentally acquire it twice, which a raw semaphore's API does). Either is an acceptable, idiomatic answer; naming both and picking one deliberately is the strong answer.

Note what this buys you over the boolean-flag approach: acquire() on an empty semaphore or await() on an un-counted-down latch blocks the calling thread in the OS scheduler — zero CPU spent, and the runtime guarantees the waiting thread sees everything the signaling thread did before it signaled (the happens-before edge from Topic 7's memory-model material). You get correctness and efficiency from the same call.

Strict alternation: a turn token, handed off on every step

Alternation is the same idea generalized to a loop, and it has two equally standard implementations:

Option 1 — semaphores handed off round-robin. Give each participant its own semaphore, initialize exactly one of them to a permit of 1 (whoever goes first) and the rest to 0, and have every participant's turn end by releasing the next participant's semaphore:

turn_foo = semaphore(1) # foo goes first turn_bar = semaphore(0) # thread "foo", repeated n times acquire(turn_foo) print("foo") release(turn_bar) # thread "bar", repeated n times acquire(turn_bar) print("bar") release(turn_foo)

This scales cleanly past two participants (Print Zero Even Odd is exactly this with three semaphores and a bit of arithmetic to decide whether zero hands off to odd or even next) and needs no explicit shared counter.

Option 2 — a single shared "whose turn is it" variable guarded by a lock and a condition variable. Every participant acquires the lock, checks a loop condition on the shared turn variable, calls wait() if it isn't its turn yet, does its work, updates the variable, and calls notify_all() so everyone re-checks:

turn = "foo" # protected by `lock`, checked/updated only while holding it # thread "foo", repeated n times with lock: while turn != "foo": wait(lock) print("foo") turn = "bar" notify_all(lock)

This generalizes even more naturally to conditions that aren't simple round-robin — Fizz Buzz Multithreaded is the canonical example: four threads each wait on a predicate over a shared counter (count % 15 == 0, count % 3 == 0 && count % 5 != 0, etc.) rather than a fixed turn order, which is exactly the guarded-block pattern from Topic 5 applied to four roles instead of two.

Both options are correct and idiomatic; semaphores are usually less code for a fixed, known number of participants passing a single token, while a condition variable scales better when the "whose turn" logic is a genuine predicate rather than a round-robin sequence.

Pitfalls and interview gotchas

  • Sleeping to fake ordering. Covered above — always call this out explicitly if you see it, even in your own scratch code, and remove it before you'd call a solution done.
  • Forgetting while, using if, around a wait(). A condition variable can wake up without the condition actually being true yet (a "spurious wakeup," or simply because a different waiter's turn just wasn't yours) — the wait must always be inside a loop that re-checks the predicate, never a one-shot if. This is Topic 5's guarded-block rule, and it applies here without exception.
  • Reusing a one-shot latch. CountDownLatch (Java) and similar one-shot primitives cannot be reset and re-armed; if you need the same ordering constraint to repeat (i.e., you've actually got an alternation problem, not a one-time ordering problem), reach for a semaphore or condition variable instead.
  • Off-by-one on who starts first. In the semaphore hand-off pattern, exactly one semaphore must start with a permit — get this wrong and every thread deadlocks immediately, waiting on a semaphore nobody will ever release.
  • Solving a 3+ role problem with only 2 semaphores by overloading state in a shared counter without protecting the counter itself. The counter update and the semaphore hand-off need to be part of the same atomic step (or the counter needs its own lock) — see Topic 2's critical-section material.

How this differs from a barrier / rendezvous

If your constraint is "one specific thread runs before another," or "these N threads take fixed, repeating turns," you're in this subtopic. If instead your constraint is "these threads, playing different roles with different required counts, must all arrive together and bond as a group before any of them proceeds" (two hydrogens and one oxygen, not "whoever's turn it is") — that's a step up in structure, and it's the next subtopic: multi-party rendezvous.

Reference implementations in:

Print in Order: One-Shot Semaphore Gates

Each gate starts locked (0 permits) and is acquired once, released once — the pattern generalizes to any fixed number of ordering constraints by chaining more gates.

type Foo struct { second chan struct{} third chan struct{} } func NewFoo() *Foo { return &Foo{second: make(chan struct{}), third: make(chan struct{})} } func (f *Foo) first(printFirst func()) { printFirst() close(f.second) // one-shot gate: unblocks second forever } func (f *Foo) second(printSecond func()) { <-f.second printSecond() close(f.third) } func (f *Foo) third(printThird func()) { <-f.third printThird() }

An unbuffered channel (or closing one) is the one-shot semaphore gate. first never blocks; second/third block on receive until the predecessor closes. Don't busy-spin on an atomic flag — that's the red-flag answer this problem is designed to catch. Closing twice panics, so each gate is used exactly once.

FooBar Alternation: Turn Variable Behind a Lock + Condition Variable

Generalizes past two participants by checking the turn variable against more values — this is exactly Print Zero Even Odd with a slightly richer predicate.

type FooBar struct { n int fooTurn chan struct{} barTurn chan struct{} } func NewFooBar(n int) *FooBar { f := &FooBar{ n: n, fooTurn: make(chan struct{}, 1), barTurn: make(chan struct{}, 1), } f.fooTurn <- struct{}{} // foo holds the token first return f } func (f *FooBar) foo(printFoo func()) { for i := 0; i < f.n; i++ { <-f.fooTurn printFoo() f.barTurn <- struct{}{} } } func (f *FooBar) bar(printBar func()) { for i := 0; i < f.n; i++ { <-f.barTurn printBar() f.fooTurn <- struct{}{} } }

A capacity-1 channel is the turn token: receive to take the turn, send to hand it over — same idea as Kotlin's Channel, and more idiomatic than sync.Cond plus a fooTurn bool. sync.Cond still works (loop on the predicate, never a bare if) but you will almost never see it in a Go solution to this problem.

Further Resources (Optional)

Practice Problems

Interview relevancy:!!!Critical — must-have!!Important — highly recommended!Good to have~Niche — rarely asked

Optional Practice (Extra Reps)

For once you've cleared the main set above and want more reps on this pattern. These don't count toward the roadmap's progress stats — solve them purely for your own benefit.