Concurrency lab
Step through concurrent programs one operation at a time, rewind, and try other interleavings: Go goroutines, channels, select, WaitGroup, Mutex and context (with a race detector, deadlocks and leaks), async Rust futures on a tokio-like executor, Java virtual threads on carriers with pinning, executors and CompletableFuture, and the JavaScript event loop — plus a runner that shows the real order of your own JS snippet.
Added
Works with
- Coding interviews Interview preppractise the algorithm rounds that come alongside concurrency questions in interviews
How to use it
Concurrency bugs depend on the order things happen in, and a real program won’t show you that order or let you change it. Here each program runs in a small simulator with a scheduler you control: step forward and back, let it pick at random, or choose which goroutine, task or thread runs next and watch the result change.
How to use it
-
Pick a language and a scenario. The code is on the left; coloured tags show where each goroutine, thread or task is about to run.
-
Step (or the → key) runs one operation; Back (←) rewinds; the slider and the list of steps at the bottom jump anywhere.
-
Run next lists everything that could run at this point. The highlighted button is what the scheduler would pick; press another to take a different path from here. The run is recomputed from that choice on.
-
Scheduler: random with a seed gives a different but repeatable interleaving; New seed tries another. On scenarios with a correct answer, Find a failing interleaving searches random seeds until the result is wrong.
-
Share link copies the scenario, the scheduler, your choices and the step you are on.
The default scheduler lets the current goroutine or thread keep going until it blocks, which is often what a real one does with short critical sections, and is why races hide in testing.
Go
Goroutines, unbuffered and buffered channels (with their buffers and the queues of waiting senders
and receivers drawn), close and range, select with default and time.After, sync.Mutex,
sync.WaitGroup, and context.WithTimeout. When every goroutine is blocked and no timer is
pending, you get Go’s own fatal error: all goroutines are asleep - deadlock!; when main returns
while goroutines are still blocked, they are listed as leaked.
A happens-before race detector (vector clocks, like go run -race) watches the shared variables.
It reports the data race in counter++ even on a run where the answer came out right, and reports
nothing for the check-then-act example — which is still wrong, because each access is locked but
the check and the act are not under the same lock.
Rust async
An async fn is a state machine; the executor polls it. The task runs until an .await isn’t
ready, registers its waker and returns Pending; when the timer fires the waker puts it back on the
run queue. The table shows each task’s state and how many times it has been polled, and the
timeline shows which worker thread ran what.
-
join!runs futures inside one task: they take turns and never run in parallel.tokio::spawnmakes separate tasks that two workers run at once. -
A blocking call (
std::thread::sleep, a big synchronous computation, blocking I/O) inside an async task holds the worker thread, so other tasks' timers fire with nobody to poll them.spawn_blockingmoves it to the blocking pool.
The executor is simplified: one global FIFO run queue, no work-stealing or LIFO slot, and a poll takes no time except where the code does work.
Java
Platform threads in a fixed pool against virtual threads mounting and unmounting on carrier
threads; pinning when a virtual thread blocks inside synchronized on JDK 21–23, and the same code
on JDK 24+ where JEP 491 removed that; a single-thread executor that deadlocks waiting for a task
queued behind itself; which thread runs each stage of a CompletableFuture chain; and an unsafe
lazy initialisation that creates two objects.
JavaScript
The call stack, the microtask queue (promise reactions, queueMicrotask, and the rest of an async
function after await), the task queue (setTimeout) and animation frames. The orders shown for
the scenarios were checked against node’s real event loop in the tests.
Run your own snippet runs your code for real, in a sandboxed frame that can’t reach this page, and
lists every console.log in your browser’s order, labelled with what was running: the script, a
task, a microtask, the continuation after an await, or an animation frame.
What these are, and aren’t
They are models, written to behave like the real runtimes on these examples, not the runtimes themselves. Timings are in virtual milliseconds. `select’s random choice uses the seed. Go’s scheduler, Tokio’s and the JVM’s all make different choices from these schedulers in detail; the point is that the program has to be correct under every interleaving, and here you can reach them.
The tests in tests/tools/concurrency-lab.test.js check the outcomes: lost updates found by random
search and reproduced by replaying the choices, the deadlock and leak reports, starvation timings,
pinning, and the JavaScript orders against node.
Related tools
-
Memory layout — what a Go channel, slice or interface, or a Rust
Arc<Mutex<T>>, looks like in memory. -
Queueing — how waiting time grows as a pool of workers fills up.
-
Latency numbers — what the operations being waited on cost.
Feedback is a gift. Something to improve, a feature to add, or a bug? Tell me.
Runs entirely in your browser; nothing you type leaves this page.