Everything the adaptive question bank can teach and test in Concurrency, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
A. Concurrency, parallelism and units of execution
The distinction decides what a change can buy you: concurrency improves how a system waits, parallelism improves how fast it computes. A single-core machine can be highly concurrent and not parallel at all.
Sharing the heap is both the feature and the hazard, and it is why the whole subject exists. Separate address spaces make a race impossible by construction, which is why process isolation is still the strongest answer available.
The line between private and shared is the first thing to establish when reading concurrent code: a local variable needs no protection, and a field reachable from two threads needs all of it. Most confusion is about which side something is on.
Cooling the caches is the cost people forget — the kernel crossing is measurable, the lost locality is often larger. It is why the fix for a blocking-heavy workload is fewer threads with more in flight, not more threads.
Removing the syscall per switch is what changes the economics, so blocking becomes cheap enough to use as the programming model again. The catch is that a blocking call the runtime does not know about still pins its carrier.
Starting small and growing is what makes the count affordable, and it means deep recursion in a goroutine is a heap cost rather than an immediate overflow. The scheduler, not the kernel, is the limit you eventually hit.
Unmounting only happens at a call the runtime recognizes, so anything else pins the carrier and defeats the scheme. A native or foreign call still does; a blocking synchronized block did too through Java 23, until JEP 491 removed that in Java 24. Check the version before assuming scale.
One non-yielding task freezing its peers is the failure mode, and it arrives as an unresponsive system rather than an error. A tight compute loop inside an event loop or an older Go runtime is the usual cause.
The cap is on the SERIAL fraction, which is why profiling the sequential part pays better than adding cores. It also explains why a benchmark that scales linearly on four cores can flatten completely on sixty-four.
B. Shared state, race conditions and data races
Defining the unit precisely is what makes per-field locking sound: two threads writing different fields of one struct are not racing. Adjacent bit-fields are the exception, because they share a location and therefore must share protection.
All three conditions are required, which is why two concurrent READS are never a data race and why adding any happens-before edge removes one. Check the conditions rather than guessing from how the code looks.
The terms are routinely swapped, and the difference is practical: a tool can find data races mechanically, while a race condition is a logic bug no sanitizer will report. Lock-protected check-then-act passes every race detector and is still wrong.
Atomicity of each step buys nothing here, because the gap between them is the bug. The fix is to widen the atomic region — a lock around both, or a compare-and-swap that fuses the test into the write.
Three operations wearing one piece of syntax is what makes this so easy to miss, and the increment is the canonical case rather than a special one. Any read-modify-write has the same shape.
Defining it by the INVARIANT rather than by the code is the useful habit: the section has to span every step that leaves the invariant broken. A section that ends mid-update is the commonest locking error.
Licensing the compiler to assume it never happens is the part that bites: the optimizer may delete the check you added, because reaching it would have required a race. The damage is not confined to the racing variable.
Bounded damage is a deliberate implementation restriction, not a license: the program is still wrong, and a torn interface or map write can still panic. It means a racy Go program can be debugged rather than that it can be shipped.
The defense fails because the compiler may reload or split the read, so the value you reason about need not exist. If a race is genuinely acceptable, say so in code with a relaxed atomic rather than with a plain access.
The prohibition is what makes an array of per-thread counters, or a struct of independently locked fields, legal at all. Without it every neighboring field would need the same lock, and most fine-grained designs would be unsound.
C. Atomicity, visibility and ordering
Naming all three separately is what stops the usual half-fix: an atomic gives you atomicity and ordering for one location, and nothing about an invariant spanning two. Ask which of the three a given primitive actually supplies.
Indivisibility is about what other threads can OBSERVE, not about how the hardware implements it, so a lock-based operation is atomic in exactly the same sense as a single instruction. The guarantee is the abstraction, not the mechanism.
No automatic propagation is the point people find hardest, because single-threaded intuition says a write simply happens. Without a synchronizing operation there is no moment at which the value becomes visible.
Order is per-observer rather than absolute, which is why reasoning about concurrent code as a single interleaving is only valid once you have established that there is no race. The unordered pairs are precisely where the surprises are.
Preserving the single-threaded result and nothing else is the whole license, so every transformation that looks illegal is legal in the absence of an edge. The compiler is not being adversarial; it is being correct under its own rules.
Independent of the compiler is why source-level reasoning is not enough: a correctly compiled program still needs the barriers the memory model implies. Reading the assembly answers one half of the question only.
Seeing one half of each of two writes gives a value that was never stored by anyone, which is the most disorienting outcome in the chapter. In Java the fix is volatile; in C++ an atomic; the plain access has no guarantee.
Fixing two of the three requirements is exactly what makes this the most common wrong fix in Java: the symptom of a visibility bug disappears while the lost-update race stays. Use an atomic class or a lock for read-modify-write.
Same keyword, unrelated meaning, and the confusion travels in one direction — Java programmers reaching for it in C++. It suppresses optimizations on that access and creates no inter-thread ordering whatever.
Spinning forever is the observable result, and it is a correct optimization rather than a compiler bug: nothing in the loop could change the flag. An atomic or volatile read is what forces the reload.
D. Happens-before and what a memory model promises
Agreement between three parties is the purpose: without a written contract the compiler's license and the programmer's expectation have no common ground. It is a specification, not a description of any particular machine.
Partial is the operative word — most pairs in a concurrent program are simply unordered, and that is where every bug is. Asking which specific edge orders two accesses is the central question of the whole subject.
Stating it as what a read MAY see, rather than what it will see, is what makes the model usable: you reason about the set of permitted outcomes. A program is correct when every permitted outcome is acceptable.
Nothing across threads is the part to internalize: your own program order is real to you and invisible to everyone else. Every cross-thread guarantee has to come from a synchronizing operation, never from the source layout.
Chaining is what makes handoffs through intermediaries safe, and it is also what makes a missing edge contaminate a long chain. Trace the edges end to end rather than checking the two threads you are thinking about.
The pairing is required: a release with no acquire that reads it creates no edge, which is why publishing through an unsynchronized field never works. Both halves have to touch the same location.
The guarantee is conditional, and that condition is the reason race-freedom is worth the effort: it buys you the right to reason simply. A racy program has no simpler model to fall back on, so debugging it by reasoning is unsound.
One global order is the model people assume by default, and the strongest that any practical hardware offers. Every weaker ordering in this chapter is a deliberate trade of that intuition for speed.
No promise of eventual arrival is what makes a spin on an unsynchronized flag unfixable by waiting. In practice the value usually arrives, which is why this failure is rare, intermittent and extremely hard to attribute.
Forbidding self-justifying values is what keeps the model from permitting outcomes no execution could produce. It constrains speculation, and it is the reason the Java model is phrased as causality rather than as simple reordering.
Constraining the compiler is the half people forget, and it has a concrete payoff: a write cannot be speculatively hoisted out of its guard, so a conditional store cannot become unconditional. Without that rule, guarded publication would be unsound.
E. The Go memory model's synchronization edges
The edge covers the arguments evaluated at the go statement, which is why passing a loop variable by value is safe and closing over it was not before Go 1.22. It says nothing about anything written afterwards.
Exit being ordered with nothing is the asymmetry to remember: starting a goroutine gives you an edge for free, finishing one gives you none. Every result a goroutine produces needs an explicit join to be visible.
The direction is send-to-receive, so the receiver safely sees everything the sender wrote beforehand. That one edge is what makes passing a pointer down a channel a legitimate ownership transfer.
Both directions is what makes an unbuffered channel a rendezvous rather than a queue: the sender learns the receiver arrived. A done-signaling channel relies on exactly this, and a buffered one would not give it.
The offset by capacity is what turns the channel into a counting semaphore, and the pattern is worth recognizing: a buffered channel of empty structs is the idiomatic Go rate limiter.
Close is a broadcast, so one close releases every waiting receiver with a proper edge — which is why it is the idiomatic cancellation signal. Closing twice or sending after close panics, so one owner must own the close.
The n-th and (n+1)-th phrasing is what makes the guarantee precise: it is between successive critical sections, not between a lock and arbitrary later code. That is the whole content of what a mutex gives you besides exclusion.
Both directions are specified, which is what makes a read lock a real synchronizing operation rather than just a concurrency permit. A reader therefore sees everything a previous writer wrote, with no extra atomics.
Every Do returning with the edge, in every goroutine, is what makes it correct for lazy initialization — the late callers see the fully built value. A hand-rolled check-then-act flag gives exclusion without that visibility.
The edge is on the Wait RETURNING, so results written before each Done are visible after it. Calling Add after the goroutine has started is the usual bug, because the counter may already have reached zero.
Offering no weaker variant is a deliberate design choice rather than an omission: Go trades the last increment of performance for a model one can hold in mind. Porting C++ relaxed code to Go therefore gets stronger, never weaker.
This is what makes a package-level variable initialized in init safe to read from any goroutine without synchronization. It is also why initialization order between sibling packages is not something to rely on.
That being the ONLY ordering is the warning: a finalizer gives you no edge to anything else, runs on its own goroutine, and may never run at all. It is not a destructor and should not carry release logic.
Where the model stops, the documentation starts — so a guarantee you need from sync.Map, a pool or a condition variable has to be read from its own doc rather than inferred. Assuming a library edge the spec does not state is how subtle races ship.
F. Java's memory model and threading contract
And only that monitor is the operative clause: locking a different object gives no edge whatever, however correct the code looks. Two methods guarding the same state must agree on which object they lock.
The two not excluding each other is the surprise, and it is a genuine bug source when static and instance methods touch the same state. Choose one explicit lock object rather than relying on what the keyword happens to pick.
The edge is per FIELD, so a volatile flag publishes everything written before it and nothing about any other field's later writes. That is what makes the write-data-then-set-flag idiom work, and only in that order.
The reference, never the elements, is the trap — and the declaration reads as though it covers the contents. Element access needs an atomic array class or a lock; marking the field volatile changes nothing about them.
Both edges together are what make the simple fork-join pattern safe with no other synchronization: arguments are visible going in, results coming out. A successful join is required — an interrupted one gives no edge.
Visible without synchronization is a strong and unusual guarantee, and it is the reason an immutable object is safe to share freely. It applies to a PROPERLY constructed reference, which is exactly what the next entry voids.
Leaking this publishes the object before its fields are frozen, so another thread can see a partly built instance. Starting a thread or registering a listener in a constructor is the common form, and it looks entirely innocuous.
The JVM already serializes it, so the holder idiom gets laziness and thread safety with no code of your own — which is why double-checked locking is unnecessary in Java for this case. Simpler, and impossible to get subtly wrong.
A request, never a stop, is why ignoring InterruptedException breaks cancellation across a whole system: swallow it and the request is lost. Restore the flag or propagate the exception; do not catch and continue.
Releasing locks with invariants half-updated is the reason it was removed rather than fixed — there is no safe moment to kill a thread from outside. Cooperative cancellation is the only sound mechanism, which is what the interruption entry provides.
G. C++ memory_order and fences
Being the default is the practical point: code with no order argument is already correct and merely slower. Weakening it is an optimization to justify with measurement, not a starting position.
Cannot publish the data it flags is the whole limitation — the flag arrives and the payload may not. It is right for a statistics counter whose value nothing else depends on, and wrong for anything that signals readiness.
It is a one-way barrier: earlier accesses cannot move past it, later ones may move before it. That asymmetry is what makes it cheap, and why a release alone without its acquire gives nothing.
The mirror of release, and also one-way. Together they form the edge; each alone is a half-fence that orders one side of one thread and creates no cross-thread guarantee at all.
Actually reading the value is the condition people skip: if the acquire loads a different write, there is no edge even though both operations have the right order argument. The data flow is what creates the synchronization.
Doing both halves in one operation is what makes it the right ordering for a lock's own state or a reference count that releases. A plain acquire or release on a read-modify-write orders only one side.
Extending the release through later read-modify-writes is what makes a reference count correct: the thread that drops the last reference acquires everything every previous holder released. A plain store would break the chain.
Per-object coherence survives even relaxed ordering, which is the one thing you can still rely on there: a thread cannot see a value go backwards. It says nothing about the order relative to any other object.
Specified but strengthened everywhere means using it buys nothing today and commits you to a subtle dependency-tracking rule. Write acquire; that is what you get in any case.
Not attached to an object is both its power and its difficulty: it orders everything around it, so reasoning is less local. Prefer ordered operations on the specific atomic, and reach for a fence when no single object carries the ordering.
Knowing WHICH direction costs money explains the asymmetry people measure: on x86 a seq_cst load is nearly free and the store is not. That is why moving a hot store to release ordering is the optimization that actually pays.
Failing only on ARM or POWER is the dangerous property: the bug is absent from your laptop and present on the server or the phone. Test on a weakly ordered machine, or use a tool that models the memory order rather than the hardware.
H. Mutual exclusion and lock design
Thinking in invariants rather than in fields is what prevents the commonest locking bug: two correctly locked fields whose relationship nothing protects. Write down what must always be true, then find the smallest region that can break it.
The read side is where people economise and it is not optional — without the lock there is no edge, so the reader may see a half-update or nothing at all. A lock is a protocol both sides have to follow.
An early return, a break, or a thrown exception are the paths that get missed, and the symptom is a hang far from the cause. Use the language's scope-bound form so there is no path to forget.
The keyword is shorter and cannot be interrupted or timed out, which is exactly when to reach for the explicit lock: anywhere a wait needs a deadline or must respond to cancellation. Fairness is also only available on the explicit one.
Counting per owner is what lets a public method call another public method on the same object without deadlocking — so reentrancy is a composability feature rather than a convenience. The next entry is what happens without it.
Go and C++ differ from Java here, and the failure is immediate and total rather than subtle. It is why a Go method that takes a lock should not call another exported method of the same type; split the locked part into an unexported helper.
Only pays off when reads are frequent AND long, because the bookkeeping is more expensive than a plain mutex. For short reads a plain lock is usually faster, which is the measurement people skip.
An unbroken reader stream blocking a writer indefinitely is why fairness matters here more than on a plain mutex. Check which policy your implementation uses; Go's RWMutex blocks new readers once a writer is waiting, and not every one does.
Deadlock rather than a slow path is the outcome, because each holder waits for the other to release a read lock neither will. Re-validating after the re-acquire is the part people drop, and the state may genuinely have changed.
Two context switches is the honest threshold, and it is why hand-rolled spin locks usually lose: a real mutex already spins briefly before parking. Spinning in userspace on a contended lock burns the core that could have run the holder.
I. Deadlock, livelock, starvation and priority
All four at once is the useful part, because it gives four independent places to intervene rather than one. Lock ordering breaks circular wait; a timeout breaks no-preemption; taking all locks at once breaks hold-and-wait.
Costing nothing at run time is what makes it the default answer: it is a convention, enforced by review or by a lint, with no overhead. Write the order down somewhere a reader will find it, or it will drift.
The two calls look symmetric and that is the trap — nothing in either function is wrong on its own. Ordering by a stable key means the address or the id, never the argument position, which varies per call.
Try-and-back-off removes the need for a global order, which matters when the set of locks is not known statically. The cost is that it may acquire and release repeatedly under contention, so a fixed order is still preferable when one exists.
Nobody is blocked, which is what makes it hard to spot: every thread is running and the system does nothing. Randomized back-off is the fix because the problem is the symmetry, not the contention.
Runnable but never winning is the distinction from deadlock, and it means the system looks healthy in aggregate while one participant makes no progress. Barging locks are fast and starve; fair locks are slower and do not.
Three threads are needed, which is why it is so rarely reproduced in testing — the medium-priority thread is the one nobody thinks to include. It is the failure that nearly lost Mars Pathfinder.
Raising the holder is the fix because the holder is the one that must finish, and it is the reason real-time systems demand that their mutexes support it. Without it, priorities on a shared lock can invert the whole schedule.
An unknown second lock in an unknown order is the hazard, and the caller cannot reason about it at all. Copy what the callback needs, release the lock, then call out — the pattern is worth applying even when today's callback is harmless.
Only TOTAL deadlock is detected, so the common case — two goroutines stuck while the rest of the program runs — produces silence. A goroutine dump is the tool for that, and it is worth knowing how to trigger one before you need it.
J. Condition variables and waiting
Blocking instead of re-testing is the whole point, and it is why a condition variable is not interchangeable with a sleep-and-poll loop: the wake comes from the state change rather than from a timer.
Both sides under the same mutex is non-negotiable, because the predicate is ordinary shared state. Testing it outside the lock reintroduces the check-then-act race that the condition variable exists to resolve.
Indivisibility is what makes a wakeup impossible to lose: there is no window between releasing the mutex and being on the wait queue. That property is the reason the mutex must be passed into the wait rather than released beforehand.
A while, never an if, and the next two entries are why: a return from wait is not evidence the predicate holds. This is the single most common condition-variable bug and it is a one-word fix.
Permitted by specification rather than a bug to work around, so the loop is required even on an implementation you believe never does it. Relying on its absence makes the code correct by luck and portable nowhere.
The thief is the real reason the loop is needed in practice: the notified waiter has to re-acquire the mutex, and another thread can take the condition first. This happens on correct implementations, unlike a spurious wake.
Both conditions are required, and mixed predicates on one condition variable is where notify-one silently hangs the wrong waiter. When in doubt notify all, and fix it only if the wake-up storm is measured.
Signaling before anyone waits is the half people miss, because the notify looks successful. The state change must happen under the mutex so that any waiter either sees the new state or is already on the queue to be woken.
Releasing no monitors is the point: a sleep inside a critical section blocks every other thread for its full duration. If waiting is the intent, wait on the condition variable, which does release the mutex.
K. Semaphores, latches and barriers
A counter rather than a flag is the generalization, and it is why one primitive covers mutual exclusion, resource pools and rate limits. What it does NOT carry is ownership, which the next entry makes concrete.
No ownership means no reentrancy, no owner checking, and no detection when a permit is released twice — so an accidental extra release silently raises the limit. Use a mutex where exclusion is the goal and the holder is the releaser.
Bounding in-flight work is the use that pays most often, because the alternative failure is resource exhaustion under load rather than a wrong answer. Size it to the resource, and acquire before starting the work rather than inside it.
Never resettable is a feature: a one-shot gate cannot be reopened by mistake, so it models start-up and completion exactly. Needing to reuse it is the signal that a barrier or a semaphore is the right primitive instead.
Re-arming is what distinguishes it from a latch and what makes it right for iterative work, where every party must finish round k before any starts k+1. The party count is fixed, which is the constraint in the next entry.
A hang with no error is the outcome, and the count is usually wrong because a party failed to start rather than because it was miscounted. Give the barrier a timeout, or the failure of one worker becomes a stall of all of them.
Calling Add inside the goroutine races with Wait, which may already have observed zero and returned. Add before the go statement, in the same goroutine that will call Wait.
One word, two unrelated mechanisms, and the ambiguity reaches real conversations: a question about a barrier needs the context before it can be answered. Say synchronization barrier or memory fence to be unambiguous.
L. Atomics, CAS and non-blocking progress
Being unsplittable is what removes the lost-update race for ONE location — and that is the whole scope of the guarantee. An invariant spanning two atomics still needs a lock or a transaction.
Reporting which happened is the part that makes it composable: the failure return hands you the current value, so the retry needs no extra read. That is why it is the primitive every lock-free algorithm is built from.
Recognizing the shape is most of lock-free programming, and the discipline is that the computed value must depend only on what was read. Carrying state across iterations is how a retry loop becomes subtly wrong.
Spurious failure is fine inside a loop you were going to retry anyway, which is why the weak form costs less on ARM and POWER. Outside a loop, use the strong form; a single weak attempt can fail for no reason.
The CAS succeeding is the whole trouble: the comparison is on the VALUE, and the value cannot tell you whether the world changed and changed back. It is why a pointer-equality check is not a state check.
Three remedies with different costs: a tag needs the space, a double-width CAS needs the instruction, and deferred reclamation needs the machinery in the next chapter. Choosing one is usually decided by which the platform gives you.
SOME thread always progressing is weaker than it sounds — an individual thread can still be starved indefinitely. The guarantee is about the system, which is enough to rule out deadlock and not enough to bound latency.
Bounded steps PER THREAD is the strong guarantee, and it is what a real-time deadline actually requires. It is also much harder to implement, which is why most production structures are lock-free rather than wait-free.
Progress only in isolation makes it the weakest of the three, and useless without a contention manager to arrange that isolation. It appears mainly as a stepping stone in algorithm design rather than in shipped code.
Surviving a descheduled or killed participant is what matters most in practice, more than the speed: a lock held by a dead thread is held forever. That is the reason to use these structures across process boundaries or in signal handlers.
Composability is the selling point and the reason it keeps being revisited: two correct locked operations do not make a correct combined one, and two transactions do. Its cost is the bookkeeping and the retries under contention.
M. Caches, coherence and the cost of sharing
Nothing operates on individual variables, which is the single fact behind every entry in this chapter. Layout is therefore a concurrency concern, not just a performance one.
Invalidating every other copy before writing is why a write is more expensive than a read under sharing, and why the cost scales with the number of sharers. A read-mostly variable is cheap; a written-by-all variable is not.
Unrelated variables is the point — the code has no logical sharing at all, so the slowdown has no visible cause. Two per-thread counters in adjacent array slots is the canonical case, and it can cost an order of magnitude.
Using the platform's own constant beats guessing 64, since the value differs by architecture and can be larger than one line. Pad only what is actually written by different threads; padding everything wastes cache.
Visible to its own core first IS the store-load reordering, which makes this the hardware explanation for the one direction x86 must fence. Everything surprising about seq_cst cost on x86 follows from this buffer.
Exclusive ownership per operation is why contended atomics stop scaling: the cost is cache traffic rather than instructions. An atomic that one thread touches is nearly free; the same atomic under sixty-four threads is a bottleneck.
Trading an expensive read for a scalable write is the right trade whenever reads are rare, which is the usual shape for metrics. The cost is that the total is only approximate while writes are in flight.
Placement changing throughput means the same code can be twice as fast on the same machine, decided by where its memory was allocated. First-touch allocation is the usual mechanism, so the thread that initializes a buffer matters.
Sharing execution units is why the thread count the OS reports overstates the parallelism available: two hardware threads give perhaps a third more throughput, not double. Size CPU-bound pools by physical cores unless measurement says otherwise.
N. Concurrent data structures and safe reclamation
Each call atomic, every SEQUENCE racy, is the limitation that makes these wrappers nearly useless: a get-then-put is exactly as broken as before. They remove data races and leave every race condition intact.
They exist because the caller cannot build them, which is the reason to learn the specific methods rather than reaching for a lock around two calls. A compute is both correct and faster than external synchronization.
Never throwing is the trade: you get an iterator that cannot fail where the non-concurrent one would, in exchange for not knowing exactly which snapshot you are reading. Do not use one to compute a total you will act on.
Per-bin locking is why the concurrent map scales, and it is also why its size is approximate under concurrent writes — there is no single place that knows the count. Scalability and exact aggregate answers are the trade.
Copying the whole array per write bounds this to read-mostly data, and the read side needs no lock at all. One write per request makes it quadratic in traffic; use a concurrent map instead.
Blocking on both ends is what makes it the complete handoff primitive, so producers and consumers need no shared lock of their own. The optional bound is the whole design decision, which the next entry covers.
Converting overload into latency and memory growth is what unbounded does, and it is worse than rejection because the failure arrives late and far away. A bound makes the pressure visible at the point where it can be handled.
Freeing being the hard part is the thing to know before writing any lock-free structure: unlinking a node is easy and deciding when nobody can reach it is not. Garbage-collected languages hide this problem, which is why it surprises people porting to C++.
Publishing what you are using inverts the question from when is it safe to free to who is still reading — a per-reader cost paid on every access. It bounds memory precisely, which is its advantage over deferring reclamation.
Readers paying nothing at all is the extreme of the trade, with the whole cost moved into the writer's grace period. It is why the Linux kernel uses it for read-mostly structures, and why writes there are deliberately rare.
Readers retry and writers never block, which is the inverse of a reader-writer lock. It only works for data you can read speculatively and discard — never for anything with a side effect, and never for pointers you would dereference.
O. Thread pools, executors and work stealing
The thread costing more than the task is the whole justification, and it is why the advice changes completely once threads are cheap. Measure the ratio before assuming a pool is needed.
One per core, because extra workers cannot create compute capacity and do add switching and cache pressure. Oversizing a CPU-bound pool reliably makes it slower, which is the counterintuitive part.
The wait-to-compute ratio is the formula, so a workload that waits nine times as long as it computes wants roughly ten workers per core. Guessing a round number is how pools end up both starved and oversized.
Hiding overload is the defect: the system reports success while queueing work nobody will wait for, and the memory grows until it fails elsewhere. A bound turns an invisible problem into an actionable rejection.
Caller-runs is the one worth knowing, because it makes the submitter do the work and therefore stop submitting — backpressure with no extra machinery. The discard policies lose work silently, which is rarely what anyone wants.
Tasks waiting on tasks in the same pool is the shape, and it hangs rather than slowing down. Use a separate pool for the dependent stage, or restructure so a task never blocks on work it submitted.
Never seen unless inspected is the trap: a task that throws disappears silently and the pool keeps running. Use execute where a failure should surface, or always consume the Future.
Two different intents, and either way the awaitTermination is required — neither call waits. Skipping it is why a process exits with work half-done and no error.
Pushing and popping the same end is what keeps the common case uncontended, so a worker never synchronizes with anyone while it has work. Stealing from the far end is what keeps the ends from colliding.
Capping the SCARCE resource rather than the threads is the inversion: the pool existed to limit thread count, and with cheap threads the real limit is connections or memory. A semaphore names that limit directly instead of encoding it as a pool size.
P. Futures, promises and async composition
Read-only is the distinction from the promise below, and that is why a future is safe to hand to several consumers. The waiting being built in is why no condition variable appears in user code.
Exactly once is the contract, so setting it twice is an error and never setting it hangs every reader. Splitting the read and write ends is what lets the producer be anywhere, including another process.
Returning the value once and leaving the future unusable is specific to std::future — a second get is undefined. Use std::shared_future when more than one consumer needs the result.
Resurfacing at retrieval is what makes the abstraction honest: a failure cannot be lost between threads. It also means a future nobody reads is a failure nobody sees, which is the next entry but one.
Or never runs at all is the trap: with deferred the work happens on the CALLING thread at get time, so discarding the future discards the work. Pass the launch policy explicitly rather than taking the default.
Joining in the destructor means std::async(f); std::async(g); runs them one after another, so the parallelism silently disappears. Keep the future alive in a named variable for as long as the work should run.
Attaching a continuation rather than parking a thread is what keeps the thread count independent of the pipeline depth. Blocking on a future inside a pool task is the shape that produces pool starvation.
What happens to the losers is the part that needs a decision: with any-of the other tasks keep running unless something cancels them, so the fast path can leave work in flight. Pair it with cancellation rather than assuming it stops them.
Q. Coroutines, async/await and event loops
Returning control rather than holding the thread is the whole mechanism, and it is why a million awaiting tasks cost almost nothing. It also means every suspension point is a place other work runs, which the next entries explore.
Where you can suspend is the practical difference: a stackless coroutine cannot suspend inside a function it called, so the whole chain must be async. A fiber can, at the cost of an allocated stack per fiber.
Every await being an interleaving point is the rule single-threaded intuition breaks on: the state you read before the await may be gone after it. Re-read what matters, or hold the invariant across the suspension deliberately.
Spreading up the call tree is why async is an architectural choice rather than a local one: one awaiting leaf eventually makes every caller async. It is the main practical complaint about the model.
Removing data races and not race conditions is the precise claim, and it is why single-threaded async code still has concurrency bugs. Two callbacks interleaving at an await can still perform a broken check-then-act.
One synchronous call stalls every task, so a CPU-bound computation or a blocking file read is a system-wide outage rather than a slow request. Move it to a worker pool and await the result.
Awaiting one after another being exactly as slow is the most common performance mistake in async code — the keyword suggests speed and provides only non-blocking. Start both, then await both, if the two are independent.
Failing with its exception discarded is the outcome, which makes an unawaited task a silent correctness hole rather than a loose end. Keep the handle, or use a scope that owns the failure.
The frame outlives the enclosing scope, so a reference captured before a suspension can dangle after it — and this is a C++ hazard in particular. Capture by value, or own the lifetime explicitly.
Readiness versus completion decides the shape of the code around it: with readiness you are told to perform the I/O, with completion you are told it already happened. Porting between the two models is a rewrite, not a parameter change.
R. Channels, CSP and actors
Being a synchronization point as well as a transfer is why an unbuffered send is the right signal for done or ready: completing it proves someone took the value. Give the same channel a capacity and that proof disappears while the code looks unchanged.
Running ahead until it fills is the benefit, and the capacity is a design decision rather than a tuning knob: it sets how much work can be in flight before the producer feels pressure.
Flow control with no extra mechanism is the reason to prefer a bounded channel to an unbounded queue: the producer slows down automatically instead of consuming memory. The block IS the signal.
Panicking rather than erroring is why ownership of the close has to be explicit in the design. When several senders exist, close a separate done channel instead, or coordinate through a WaitGroup and let one goroutine close.
Telling a genuine zero from a drained channel is the whole purpose, and omitting it is why a range over values can treat a closed channel as an endless stream of zeros. Use the two-value form whenever the zero value is meaningful.
Blocking permanently sounds like a bug and is the idiom: setting a channel variable to nil removes its case from a select for good. It is how a loop stops listening to one source without restructuring the select.
Uniform random choice is what prevents a starved case, so a busy channel cannot monopolise the select. It also means a select is not a priority mechanism; priorities need nested selects with a default.
Never collected is the point: a blocked goroutine holds its stack and everything it references forever, and nothing reports it. Every send needs either a guaranteed receiver or a cancellation case in a select.
One message at a time is what removes the lock — the serialization is in the mailbox rather than in a critical section. The cost is that the state is only reachable through messages, which is a design constraint, not an inconvenience.
Naming the recipient versus naming the channel is the architectural difference: actors know who they are talking to, CSP peers do not. That decides whether the topology is discoverable at run time or fixed by the wiring.
S. Cancellation and structured concurrency
A request the task must observe is the only sound model, for the reason the Java stop entry gives: killing a thread releases its locks mid-update. Every cancellation design is therefore about where the checks go.
One signal reaching the whole subtree is what makes cancellation composable, so a request abandoned at the edge tears down everything it started. It requires the context actually be passed down, which is why it belongs in the signature.
An absolute instant composes and a relative one does not: a timeout restarted at every hop can total far more than the client is waiting for. Pass the deadline down and let each layer derive from it.
Leaking a timer and a watching goroutine is a slow leak rather than an immediate failure, which is why it survives review. defer cancel() on the line after the derivation is the habit that makes it impossible to forget.
A blocking operation that does not watch the done channel makes cancellation a no-op, so the request returns and the work continues. Every blocking receive in cancellable code needs the second case.
Failing instantly is the trap: the cleanup call returns an error immediately and the resource is never released. Derive a fresh context with its own short deadline for the release path.
Silently losing the request is what makes a bare catch-and-continue worse than not catching at all: callers up the stack never learn. Restore the flag or rethrow; those are the only two correct handlers.
Joining in the destructor is what makes the lifetime structural rather than remembered, so no thread outlives its scope. It is the C++20 fix for the detached thread that outlives the data it referenced.
The block being unable to exit while children run is the guarantee, and it converts a whole class of leaks into a compile-time-visible shape. A task cannot be forgotten if its lifetime is a scope.
Surfacing at the scope instead of vanishing into a log is the half that fixes the fire-and-forget hole: one child's failure cancels its siblings and reaches the caller. Error handling becomes ordinary rather than a per-task concern.
T. Immutability, confinement and safe publication
No lock at any scale is the strongest result in the chapter, and it is why making state immutable is the first thing to try rather than the last. It scales perfectly because there is nothing to coordinate.
SAFELY published is the condition doing all the work — without it the readers may see a partly built object, which is the unsafe-publication entry below. The object need not be formally immutable, only finished before it is shared.
The reference constant and the contents free-for-all is the commonest false sense of safety, because the declaration reads as a guarantee about the object. Wrap or copy the collection; the keyword protects only the field.
Copying at the boundary is what keeps a caller's later mutation from becoming your race, and it matters on the way out as well as in. The cost is real, which is why an immutable type is better than a copied mutable one.
Knowing the list matters because any one of them suffices and nothing else does — a plain field store is not on it. Insertion into a concurrent collection is the one people forget, and it is often the most convenient.
The reference arriving before the fields is the mechanism, and it is why double-checked locking without a volatile is broken. Another thread sees a non-null pointer to an object whose constructor has not landed.
The design constraint replacing the lock is the point: no synchronization is needed and none can be forgotten. It only holds while the confinement does, so a single leaked reference silently converts it into shared mutable state.
Leaking into the next task is a correctness and a security problem, since the pooled thread carries the previous request's data. Clear it in a finally; the pool will not do it for you.
Stop touching it is the whole discipline, and it is unenforced in most languages — which is why Rust's move semantics and Go's channel idiom are both attempts to make it checkable. One owner at every instant is the invariant.
U. Classic bugs and how they are actually found
A non-null pointer to a not-yet-constructed object is the exact failure, and it is the unsafe-publication rule in its most famous form. The volatile or atomic reference is not optional, and in Java a holder class avoids the whole question.
Acting on the handle rather than the name is the fix and it generalizes: the check and the use must refer to the same object, not to the same identifier. A path is a lookup, so anything can be substituted between the two calls.
Capturing the variable rather than its value is the behavior, and Go changed it in 1.22 — so the same code is correct or broken depending on the version. Pass the value as an argument if either version must work.
Mid-write is the part that makes this a correctness bug rather than an inconvenience: a file or a connection is abandoned half-done. Join explicitly, or make the work survive by moving it somewhere with a lifecycle.
Rarer and slower is the worst combination, because the defect survives into production while the test suite gets longer. If a sleep makes a test pass, the sleep has identified a missing edge rather than supplied one.
The observation changing the timing is why concurrency bugs resist the usual tools, and why reproducing one is most of the work. Prefer a mechanism that forces the interleaving over one that watches for it.
Only code paths that actually ran is the limitation to plan around: a clean run proves nothing about an untested path. Run it over the integration suite rather than the unit tests, and accept the slowdown as the price.
Forcing the order is what turns an intermittent failure into a deterministic test, and it is the technique that makes a fix verifiable. A latch between two threads is usually enough; a scheduler is for the hard cases.
Blocked time rather than CPU time is the measurement that finds the real bottleneck, because a contended lock consumes almost no CPU. A flame graph of CPU can look healthy while throughput is lock-bound.
V. Consistency and progress models
Instantaneously at one point between call and return is the definition, and it is what makes a concurrent object behave like a sequential one in every history. Everything about composability below follows from the real-time anchoring.
Respecting real time is what makes linearizability LOCAL, and locality is the property that matters in practice: you can reason about one object at a time. Sequential consistency gives a global order with no such guarantee.
No global argument required is the payoff, and it is why linearizability is the standard for concurrent objects. Assembling a system from sequentially consistent parts gives you no equivalent conclusion.
It is a TRANSACTION property spanning several objects, which is a different question from linearizability's single-object one — so a system can offer one and not the other. Databases talk about this; concurrent data structures talk about the other.
No promise about anything read in the meantime is the clause that decides whether it is usable: a counter can tolerate it and a balance check cannot. Convergence once updates stop is a weaker guarantee than it sounds.
Different arguments is the practical consequence: a safety property can be checked on every state, and a liveness one needs reasoning about infinite behavior. Most tests check safety only, which is why starvation bugs reach production.
CAS being universal is the result worth carrying: it means any concurrent object can be built from it, where plain reads and writes cannot solve consensus for two threads. That is why the hardware primitive is the one it is.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.