Synchronisation vs Coordination
Synchronisation protects shared state. Coordination orchestrates the order or timing of work. Mutexes synchronise. Channels, latches, barriers, futures coordinate. Mixing them up leads to overcomplicated code that locks where it should signal, and signals where it should lock.
Two different questions, two different answers
Most concurrency confusion goes away once the problem splits into one of these two questions:
- Are two threads about to step on the same data? That's synchronisation. The goal is only one thread inside the critical section at a time.
- Does thread B need to wait until thread A has finished something? That's coordination. The goal is for B to sleep until A says go.
Both are real. They need different tools. Mixing them up is where the bad code comes from.
Two everyday pictures
Synchronisation is a single-occupancy bathroom. The lock on the door has one purpose: only one person inside at a time. There's no notion of "wait until someone specific is done." Whoever gets there first goes in. Everyone else waits in the hallway.
Coordination is a relay race baton handoff. Runner B isn't trying to be alone on the track. B is waiting for A to finish a lap and pass the baton. The lock is irrelevant; the timing is the point.
A mutex is the bathroom lock. A latch, channel, or condition variable is the baton.
Side-by-side: which tool for which job
SYNCHRONISATION COORDINATION
"one at a time" "wait for a signal"
Mutex Y primary use X wrong tool
RWLock Y primary use X wrong tool
Atomic / volatile Y primary use (one var) X wrong tool
Semaphore (count=1) Y acts like a mutex - awkward
Semaphore (count=N) - rate-limit only Y N-permit signaling
CountDownLatch X wrong tool Y "wait until ready"
CyclicBarrier X wrong tool Y "all N arrive, then go"
Condition variable Y holds the lock Y wait for predicate
Future / Promise X wrong tool Y "wait for a result"
Channel (Go) Y via send>recv ordering Y "wait for a value"
Event (Python) X wrong tool Y "wait until set"
Queue / BlockingQueue Y protects internal storage Y producer-consumer
The middle column ("primitives that do both") is real. A condition variable holds a lock (synchronisation) and lets a thread sleep on a predicate (coordination). A channel send orders memory (synchronisation) and delivers a value (coordination). A blocking queue does both at once.
That overlap is useful in design. It also means knowing which jobs each primitive handles cleanly and which ones must be bolted on.
A simple test for which one is needed Say the problem out loud in one sentence.
- "These reads and writes must not collide" → synchronisation. Reach for a mutex or atomic.
- "This must finish before that starts" → coordination. Reach for a latch, channel, or condition variable.
- "Both" → use a primitive that bundles them (channel, condition variable, blocking queue) or compose two.
Two failure modes from mixing them up
Failure 1: using a lock to coordinate. The code spins inside a while loop, taking the lock, checking a flag, releasing, sleeping, retrying. Wastes CPU. Often deadlocks. The right move is a latch, event, channel, or condition variable.
BAD: spin under a lock to wait for an event
---------------------------------------------
while (true) {
lock.acquire()
if (ready_flag) { lock.release(); break; }
lock.release()
sleep(50ms) // burns CPU, races with the writer
}
GOOD: actually wait
---------------------------------------------
event.wait() // sleeps; wakes when set
Failure 2: using a channel where a lock would do. Three lines of mutex-guarded counter become twenty lines with a goroutine, a channel, a select, a shutdown protocol. The original problem was just "increment safely." The original tool is a mutex (or an atomic).
BAD: a goroutine + channel for what should be a mutex
---------------------------------------------
counter := newCounterActor()
counter.inc <- struct{}{} // send to actor
val := <-counter.read // ask the actor
// plus: spawn the actor, handle shutdown, drain the channel...
GOOD: just a mutex
---------------------------------------------
var mu sync.Mutex
var counter int64
mu.Lock(); counter++; mu.Unlock()
Both bugs come from not splitting the question first. Once it's split, the right primitive is usually obvious.
Implementations
The work here is mutual exclusion. Two threads must not be inside the increment at the same time. Coordination is not the goal. A mutex (or atomic) is the right tool.
1 class Counter {
2 private final AtomicLong value = new AtomicLong();
3 public void inc() { value.incrementAndGet(); }
4 public long get() { return value.get(); }
5 }The work here is sequencing. The worker should not start until setup is done. A latch or semaphore is the right tool. Trying to use a mutex for this leads to busy waiting or weird wait/notify dances.
1 CountDownLatch ready = new CountDownLatch(1);
2
3 Thread setup = new Thread(() -> {
4 loadConfig();
5 warmCache();
6 ready.countDown(); // signal: setup done
7 });
8
9 Thread worker = new Thread(() -> {
10 ready.await(); // coordinate: wait for setup
11 processRequests();
12 });
13
14 setup.start();
15 worker.start();Key points
- •Synchronisation: 'no two threads in this section at once'. Mutex, RWLock, atomic, synchronized.
- •Coordination: 'do this after that'. Channel, latch, barrier, future, semaphore, condition variable.
- •Misapplication: using a mutex to wait for an event (busy-loop with mutex). Or using a channel to protect a variable (overkill).
- •Channels are coordination AND synchronisation in one move because the send happens-before the receive.
- •When in doubt, ask what's actually needed. Mutual exclusion of access? Or sequencing of work?