Keentune

Concurrency curriculum

22 chapters
·
213 concepts
·
free
Everything the adaptive question bank can teach and test in Concurrency, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
New here? Read the Concurrency guide
A free 16-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. Concurrency, parallelism and units of execution
concurrency is interleaving independent tasks; parallelism is running them in the same instant on separate cores
processes get separate address spaces; threads of one process share the heap, so only threads can race on it
each thread owns a stack, registers and a program counter; everything reachable from the heap is shared
a switch saves state, crosses the kernel and cools the caches, which is why a thread per request stops scaling
goroutines, virtual threads and fibers are scheduled by the runtime onto a small pool of OS threads, with no syscall per switch
a goroutine starts with a few kilobytes of growable stack, so a million of them is routine where a million OS threads is not
a blocking call parks a virtual thread and returns its carrier OS thread to run something else
a cooperative scheduler advances only at yield points, so one non-yielding task freezes every task sharing that thread
the serial fraction caps the speedup: 5% serial means at most 20x, no matter how many cores you add
B. Shared state, race conditions and data races
the unit that races is one scalar object or a maximal run of adjacent bit-fields, not a whole struct
two threads access the same location, at least one of them writes, and no happens-before edge orders the pair
a race condition is a timing-dependent wrong result; a data race is an unordered access — a lock-protected check-then-act is the first without the second
the condition you tested can already be false when you act on it, even if the test and the action are each atomic
x++ is a load, an add and a store, so two interleaved increments can add one
the region that at most one thread may execute at a time for the invariant to hold at its exit
in C++ a data race makes the whole program undefined, which licenses the compiler to assume it never happens
Go's implementation restriction confines the damage to the words involved: a racy Go program is wrong but stays memory-safe
a compiler may reload, split or duplicate a racy read, so "it is only a stale counter" is not a defense
writing one field must never disturb an adjacent one, which is exactly what makes per-field locking legal
C. Atomicity, visibility and ordering
thread safety needs atomicity, visibility and ordering; a lock supplies all three, an atomic variable only some
no other thread can observe the operation half-completed
a write is not automatically observable elsewhere; some synchronizing operation has to make it so
the order one thread's operations appear to take, seen from another thread, need not be its program order
the optimizer may hoist, sink, fuse or delete accesses; the as-if rule preserves the single-threaded result and nothing else
store buffers, speculation and cache protocols reorder at run time, independently of the compiler
a non-volatile long or double write may be split in two, letting a reader see one half of each of two writes
volatile fixes visibility and ordering but not atomicity, so v++ is still a lost-update race
in C and C++, volatile means memory-mapped I/O; it orders nothing between threads and is not atomic
a loop reading a non-atomic stop flag may have the load hoisted into a register and spin forever
D. Happens-before and what a memory model promises
it states exactly which writes a read may observe, so compiler, CPU and programmer can agree
happens-before orders only some pairs of operations, and the unordered pairs are where every bug lives
a read may see any write it does not happen-after, unless another write is ordered between them
program order orders one thread's own operations and says nothing across threads
edges chain, which is how a two-hop handoff through a third thread stays safe
the cross-thread edge is created by a release operation and the acquire that reads what it wrote
a data-race-free program can be reasoned about as a simple interleaving; a racy one cannot be reasoned about at all
one global order of all operations, consistent with each thread's program order
with no edge the model never promises the value arrives "soon"; a stale read may persist indefinitely
the JMM forbids values that exist only because a speculative read justified the write that produced it
E. The Go memory model's synchronization edges
the go statement happens-before the new goroutine's first instruction, so its arguments are safely visible
a goroutine's exit is ordered with nothing; you must join with a channel, a WaitGroup or a lock
a send on a channel happens-before the corresponding receive finishes
on an unbuffered channel the receive happens-before the send returns, which is why it synchronizes in both directions
the k-th receive happens-before the (k+C)-th send completes, which turns a capacity-C channel into a semaphore
closing a channel happens-before a receive that returns because the channel is closed
for any n, the n-th Unlock happens-before the (n+1)-th Lock returns
an RUnlock happens-before the next Lock returns, and a writer's Unlock before later RLock calls return
the single completed call inside Once.Do(f) happens-before every Do returns, in every goroutine
the Done calls happen-before the Wait they release returns
sync/atomic behaves like C++ seq_cst atomics; Go deliberately offers no relaxed or acquire-only variants
F. Java's memory model and threading contract
an unlock of a monitor happens-before every subsequent lock of that same monitor, and only that monitor
a synchronized instance method locks this; a synchronized static method locks the Class object, so the two do not exclude each other
a write to a volatile field happens-before every later read of that field
declaring an array volatile makes the reference volatile, never the elements
Thread.start() happens-before the thread's first action, and its last action happens-before a successful join()
writes to final fields in the constructor are visible without synchronization to any thread that reads a properly constructed reference
leaking this from a constructor, by starting a thread or registering a listener, voids that guarantee
the JVM serializes class initialization, which is why a holder class gives lazy singletons with no explicit lock
interrupt() sets a flag or raises InterruptedException at a blocking point; it never stops running code
asynchronously killing a thread releases its locks with invariants half-updated, which is why Thread.stop is gone
G. C++ memory_order and fences
all seq_cst operations appear in one total order every thread agrees on, and seq_cst is what you get when no order argument is written
atomic and coherent, but it orders no other access, so it cannot publish the data it flags
nothing sequenced before the release may be reordered after it
nothing sequenced after the acquire may be reordered before it
the edge exists only when the acquire actually reads the value that release wrote; a release nobody reads orders nothing
a read-modify-write that acquires the value it read and releases the value it wrote
later read-modify-writes on the same atomic extend the release, so an acquirer that reads any of them still gets the edge
every atomic object has one total order of its own writes, respected even under relaxed, and no thread may see it run backwards
dependency ordering is specified but every implementation strengthens it to acquire
atomic_thread_fence orders the accesses around it without being attached to any one object
the store-load direction is the only one x86 must actively fence, which is why seq_cst stores are the expensive ones
acquire/release are nearly free on x86, so a missing order argument often fails only on ARM or POWER
H. Mutual exclusion and lock design
one lock must cover every variable the invariant spans, not one field at a time
an unlocked read of guarded state can see the invariant mid-update, or never see the update at all
release in finally, defer or an RAII guard so an early return or a throw cannot strand the lock
synchronized is block-scoped and uninterruptible; ReentrantLock adds tryLock, timeouts, interruptibility and a fairness option
a reentrant lock counts holds per owner, so a method can safely call another method that takes the same lock
sync.Mutex and std::mutex are not reentrant; locking twice on one thread deadlocks or is undefined
many concurrent readers or one writer, which only pays off when reads are frequent and long
an unbroken stream of readers can block a writer indefinitely unless the lock is write-preferring
two readers each trying to upgrade to the write lock deadlock; release, re-acquire, then re-validate what you read
spinning wins only when the expected wait is shorter than two context switches, so real mutexes spin briefly and then park in the kernel
I. Deadlock, livelock, starvation and priority
mutual exclusion, hold-and-wait, no preemption and circular wait must all hold at once; break any one and deadlock is impossible
a single global acquisition order removes the circular wait, and costs nothing at run time
transfer(a,b) and transfer(b,a) deadlock unless the two locks are ordered by a stable key such as an account id or an address
std::lock and std::scoped_lock take several mutexes with a try-and-back-off algorithm instead of a fixed order
threads keep reacting to each other and never progress; lockstep back-off is the classic cause and randomization the classic fix
a thread stays runnable but never wins the resource, because barging or priority keeps favoring others
a high-priority thread waits on a lock held by a low-priority thread that a medium-priority thread keeps preempting
the holder temporarily runs at the highest waiter's priority so it can finish and release
calling a callback or foreign code while holding a lock can acquire an unknown second lock in an unknown order
Go panics when all goroutines are asleep; a subset that is stuck just hangs silently
J. Condition variables and waiting
block until shared state satisfies a predicate, instead of burning a core re-testing it
the predicate is shared state, so testing it and changing it must both happen under the same mutex
wait releases the mutex and blocks as one indivisible step, which is what makes a wakeup impossible to lose
re-test the predicate after every wake; an if instead of a while is a bug
a wait may return with no notification at all, and implementations are permitted to do it
another thread can consume the condition between the notify and the woken waiter re-acquiring the mutex
notify one only when all waiters wait on the same predicate and any single one can consume the event
changing the predicate without the mutex, or signalling before anyone waits, drops the event and hangs the waiter
Thread.sleep and yield release no monitors, so sleeping inside a critical section blocks everyone
K. Semaphores, latches and barriers
a permit counter: acquire blocks while it is zero, release returns a permit
any thread may release a permit it never acquired, which is exactly why a semaphore is not a mutex
the standard way to bound concurrent use of N connections, files or in-flight requests
a countdown latch opens at zero and can never be reset
releases when a fixed number of parties arrive, then re-arms for the next round
a barrier expecting more parties than will ever arrive hangs every thread already waiting on it
Add must run before the goroutine starts; calling it inside the goroutine races with Wait
a synchronization barrier makes threads meet; a memory barrier orders memory accesses — one word, unrelated mechanisms
L. Atomics, CAS and non-blocking progress
an atomic read-modify-write cannot be split by another thread, so no increment is lost
write the new value only if the current one still equals the expected one, and report which happened
read, compute, CAS, retry on failure: the shape of almost every lock-free update
compare_exchange_weak may fail spuriously and is cheaper on load-linked/store-conditional machines, so it belongs inside a loop
the value went A→B→A, so the CAS succeeds even though the state it reasoned about is gone
a version tag beside the pointer, a double-width CAS, or deferring reclamation so the node cannot be reused
some thread always makes progress, whatever the scheduler does to the others
every thread finishes in a bounded number of its own steps, so no thread can be starved
progress is guaranteed only for a thread running in isolation, so it needs a contention manager
a lock-free structure keeps working when a participant is descheduled or killed; a lock does not
speculate, then commit or retry on conflict; its selling point is that transactions compose where locks do not
M. Caches, coherence and the cost of sharing
coherence and sharing operate on lines of about 64 bytes, never on individual variables
a core must invalidate every other copy of a line before it may write it
two unrelated variables in one line make independent threads bounce that line between cores
pad and align independently-written objects apart by the destructive-interference size
a store is visible to its own core before any other, which is precisely the store-load reordering
a locked instruction takes exclusive ownership of the line, so contended atomics serialize on cache traffic
per-thread cells summed only on read (LongAdder) trade an expensive read for a scalable write
memory attached to another socket is measurably slower, so thread and allocation placement change throughput
two hardware threads share one core's execution units, so doubling threads does not double work
N. Concurrent data structures and safe reclamation
locking every method makes each call atomic and leaves every multi-call sequence racy
putIfAbsent, compute and merge exist because the caller cannot make the two-call form atomic
a java.util.concurrent iterator never throws ConcurrentModificationException and may or may not reflect updates made after it was created
per-bin locking and CAS insertion are why a concurrent map scales where a globally synchronized one does not
lock-free reads paid for by copying the whole array on every write; only for read-mostly data
the producer/consumer handoff primitive: blocking take, blocking put, and an optional bound
a bounded queue creates backpressure; an unbounded one converts overload into latency and memory growth
a reader can still be inside a node the writer has already unlinked, so freeing is the hard part of lock-free design
each reader publishes the node it is using, and a node is freed only when no hazard pointer names it
readers run with no synchronization at all; the writer publishes a new version and frees the old one after a grace period
the writer bumps a counter to odd then even, and readers retry if it changed or was odd, making reads cheap and writes unblocked
O. Thread pools, executors and work stealing
creating a platform thread costs a stack and a syscall, usually far more than the task itself
roughly one worker per core; extra workers only add switching and cache pressure
size by the wait-to-compute ratio, because a blocked worker occupies no core
with no bound, excess work becomes latency and memory instead of visible rejection
abort, discard, discard-oldest or caller-runs; caller-runs throttles the submitter, which is backpressure for free
tasks that wait on other tasks in the same bounded pool can consume every worker and hang
a throw inside submit is stored in the Future and is never seen unless someone inspects it
stop accepting and drain, or interrupt what is running; either way, then awaitTermination
each worker pushes and pops its own deque end while thieves take from the other end, so the common case never contends
when threads are cheap, cap the scarce resource with a semaphore instead of pooling threads
P. Futures, promises and async composition
a placeholder for a value that will exist later, with the waiting built in
the producer sets a value or an exception into the shared state exactly once
std::future::get waits, returns the value once, and leaves the future unusable
an exception thrown inside the task resurfaces at the point where the result is retrieved
std::async with deferred runs the function on the calling thread at get time, or never runs at all
the future returned by std::async joins in its destructor, so discarding it silently serializes the program
thenApply/thenCompose attach a continuation rather than parking a thread on a result
waiting for every result versus taking the first to arrive, and what happens to the losers
Q. Coroutines, async/await and event loops
a coroutine returns control to its caller at a suspension point instead of holding the thread while it waits
a stackless coroutine can suspend only in its own frame; a fiber has a stack and can suspend anywhere in the call tree
shared state can change across every await, so invariants must hold at each one
an async function can only be awaited by another async function, so the property spreads up the call tree
one loop thread runs ready callbacks one at a time, which removes data races but not race conditions
a single synchronous call inside the loop stalls every other task on it
awaiting calls one after another is exactly as slow as blocking on each in turn
a task nobody awaits can fail with its exception discarded and its work half-done
a coroutine that resumes after an object it captured by reference has died is a dangling reference
epoll and kqueue report that a descriptor is ready; IOCP and io_uring report that the I/O is already done
R. Channels, CSP and actors
both sides block until the other arrives, so an unbuffered send is also a synchronization point
capacity lets a producer run ahead of the consumer until the buffer fills
a full channel blocks the sender, which is flow control with no extra mechanism
sending on a closed channel panics, and so does closing one twice, so exactly one goroutine must own the close
the second result tells a genuine zero value apart from a drained closed channel
send and receive on a nil channel block permanently, which is the idiom for disabling one select case
when several cases are ready one is chosen uniformly at random, which prevents a starved case
a goroutine blocked forever on a channel is never collected, so every send needs a guaranteed receiver or a cancellation case
an actor owns private state and processes one message at a time, so it needs no lock
an actor addresses a named recipient that owns a mailbox; CSP names the channel and not the peer
S. Cancellation and structured concurrency
a running task cannot be safely killed, so cancellation is a request the task has to observe
a derived context cancels when its parent does, so one signal reaches the whole subtree of work
a deadline is an absolute instant that composes down the call tree; a timeout restarts the clock at every hop
skipping the cancel function leaks its timer and any goroutine still watching the context
a blocking operation must also wait on the done channel, or the cancellation never takes effect
releasing resources with the already-cancelled context fails instantly and leaks the resource
catching InterruptedException and continuing must re-set the interrupt flag, or the request is silently lost
std::jthread requests a stop and joins in its destructor, so no thread outlives its scope
a child task's lifetime is bounded by the block that started it, and the block cannot exit while children run
one child's failure cancels its siblings and surfaces at the scope instead of vanishing into a log
T. Immutability, confinement and safe publication
state that never changes after construction cannot race, so it needs no lock at any scale
an object never mutated after being safely published is as safe as a truly immutable one
a final field holding a mutable list makes the reference constant and the contents free-for-all
copying at the boundary stops a caller's reference from silently becoming shared mutable state
a static initializer, a final field, a volatile or atomic reference, or insertion into a concurrent collection
a plain reference store can become visible before the object's own field writes, so another thread sees a half-built object
state that only one thread can reach needs no synchronization; the design constraint replaces the lock
per-thread state must be removed at task end, or it leaks into the next task that runs on that pooled thread
hand the object over and stop touching it, so exactly one thread owns it at every instant
U. Classic bugs and how they are actually found
without a volatile or atomic reference the second thread can see a non-null pointer to a not-yet-constructed object
checking a path's permissions and then opening it by name is exploitable; act on the handle you already hold, not on the name
a closure launched per iteration captures the variable, not its value, unless the language scopes it per iteration
when main returns, Go kills every remaining goroutine and the JVM abandons its daemon threads, mid-write
a sleep that "fixes" a race only makes it rarer, and the program slower
a print, a debugger or an unoptimized build changes the timing enough to hide the defect
happens-before instrumentation reports races only on code paths that actually ran, at a large time and memory cost
a latch, an injected delay or a deterministic scheduler reproduces the ordering a normal test never hits
measuring blocked time rather than CPU time is what finds the lock that is costing throughput
V. Consistency and progress models
every operation appears to take effect instantaneously at one point between its call and its return
sequential consistency ignores real time and linearizability respects it, which is why only the latter is a local property
a system assembled only from linearizable objects is linearizable, with no global argument required
a multi-object transaction property: some serial order of whole transactions explains the observed result
replicas converge once updates stop, with no promise about anything read in the meantime
"nothing bad ever happens" and "something good eventually happens" are proved by different arguments
atomic read/write solves consensus for one thread and CAS solves it for any number, which is why CAS is called universal
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All about Concurrency practice
Also on your phone
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.
© 2026 SportaApp LLC