gap·map

Concurrency

[ middle depth ]

Threads vs processes vs async

A process owns an isolated address space: separate memory, its own handles, and a crash that takes down only itself. Threads live inside one process and share its heap, which makes passing data between them free and makes data races possible in the same breath. Async is one thread juggling many I/O-bound tasks with non-blocking calls and an event loop, so nothing on that thread races, but one blocking CPU call freezes every task waiting behind it. Rule of thumb: threads or processes for CPU work spread across cores, async for fanning out thousands of network calls. (The single-threaded JS event loop has its own module; this is the language-agnostic version.)

Why race conditions happen

Three ingredients: shared mutable state, at least one writer, and no synchronization ordering the accesses. The canonical bug is a counter.

// value++ is really three steps: load, add, store
void inc() { value++; }   // two threads both load N, both store N+1 => one lost

Both threads read the same number, each adds one, both write it back, and an increment vanishes. The region that must run without interference is the critical section, and you guard it with a mutex: lock before, unlock after, always in a finally or an RAII guard so a thrown exception cannot leak the lock.

Wrong "A mutex makes the increment atomic." A mutex serializes access. You still have to hold it on every path that touches the data, reads included. Lock the writes only and an unsynchronized reader can still observe a half-updated value.

How to avoid deadlock

Deadlock needs four conditions holding at once (Coffman, 1971): mutual exclusion, hold and wait, no preemption, and circular wait. Break any single one and it cannot occur. The cheapest to break in application code is circular wait: acquire locks in one global order everywhere. If thread A takes X then Y, thread B must also take X then Y, never Y then X. Add a lock timeout (a tryLock that gives up and backs off) so a thread that cannot make progress releases what it holds instead of hanging the system forever.

[ senior depth ]

Atomic operations and lock-free code

Atomic means indivisible: no thread ever observes a partial state. The hardware primitive underneath it is compare-and-swap (CAS): read the current word, swap in a new value only if it still equals the expected one, all in a single instruction, returning success or failure. A lock-free counter is a CAS retry loop.

loop:
  old = load(x)
  new = old + 1
  if CAS(x, old, new): done   // else another thread won; reload and retry

Lock-free trades blocking for spinning. Under heavy contention that loop retries constantly, so it is not automatically faster than a mutex. Benchmark, do not assume. A CAS over a reused pointer is also open to the ABA problem: the value goes A to B and back to A between your read and your CAS, the CAS succeeds, and you never notice the world moved underneath you. The fix is a version tag packed with the pointer, or hazard pointers.

Why a write may never be seen

Atomicity is only half the story; visibility and ordering are the other half. Without a memory barrier, the compiler and CPU may reorder your stores and keep a written value sitting in a register, so a second thread spinning on a flag can loop forever reading a stale copy that never gets refreshed.

Wrong "value is volatile, so the code is thread-safe." In C and C++, volatile gives neither atomicity nor cross-thread ordering and is not a threading tool at all. Java's volatile does give visibility and ordering, yet still not compound atomicity, so a volatile count++ still loses updates. The thing you actually reason about is happens-before: a lock release, an atomic store with the right memory order, or a thread join is what makes one thread's writes visible to another.

What interviewers grade

Optimistic versus pessimistic is a contention question. Pessimistic locking blocks others up front and wins when conflicts are likely or the critical section is long. Optimistic does the work and validates at commit with a version number or a CAS, retrying on conflict, so it wins when conflicts are rare and degrades into a retry storm when they are not. Past the answer itself, the signal is whether you name the race precisely (lost update, torn read), reach for the smallest correct primitive rather than a lock around everything, and volunteer false sharing (two hot variables on one cache line ping-ponging between cores) and visibility as failure classes, not just mutual exclusion.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

unlocks

was this useful?