Crux Read small snippets — an event-loop ordering puzzle, a thread interleaving, a lost-update data race, and a lock fix — and predict the behaviour or pick the correct fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min
Concurrency bugs are read in code and in traces of what ran when. Read each snippet, work out the order things actually happen, and pick what a careful engineer would conclude — or fix — first.
Goal
Practise the core skill of this unit: trace what runs when. Predict event-loop output order, spot how two threads can interleave on shared state, recognise a lost-update data race, and identify the lock that fixes it without overreaching.
Snippet 1 — event-loop ordering
console.log("start");setTimeout(() => { console.log("timeout"); // a callback}, 0);console.log("end");
Quiz
Completed
What does this print, in order?
Heads-up setTimeout does not run the callback; it queues it. The synchronous 'end' runs before the queued callback, because a callback runs only when the call stack is empty. The 0 is a queueing delay, not an immediate run.
Heads-up A 0 delay still queues the callback; it runs after the synchronous code finishes. 'timeout' does print — it is simply last, once the stack is empty.
Heads-up Synchronous code always runs first. The timer only makes the callback eligible to be queued; the event loop will not run it until 'start' and 'end' have printed and the stack is empty.
Snippet 2 — two threads, one shared variable
shared: total = 0Thread A: Thread B: r = read(total) r = read(total) // both read 0 r = r + 1 r = r + 1 write(total, r) write(total, r) // both write 1
Quiz
Completed
Both threads run this once, in parallel, on two cores. After both finish, what can total be — and what is this called?
Heads-up That holds only if the threads do not overlap. Because read-add-write is three separate steps, the two threads can both read 0 first, then both write 1 — losing one increment. The result depends on interleaving, so it is not always 2.
Heads-up Writes do not cancel; they overwrite. If the threads happen not to overlap, you correctly get 2. The value is 1 only in the interleaving where both read the same old value. The outcome is nondeterministic, not always 1.
Heads-up A data race on a simple counter does not crash here; it silently produces a wrong number (often 1 instead of 2). The danger is exactly that it looks fine and just loses updates — a race condition, not a crash.
Snippet 3 — the same code under load
total = 0spawn 1000 threads, each does: total = total + 1 // no synchronizationjoin all threadsprint(total)
Quiz
Completed
On a multi-core machine, what is the likely printed value and why?
Heads-up Only if the increments never overlap, which is not guaranteed without synchronization. In practice many threads read the same value before writing, so updates are lost and the total comes out below 1000, differently each run.
Heads-up Threads do not reset total to 0; each writes (old value it read) + 1. Some increments do land. The result is some number below 1000, not 0 — the lost updates reduce it, they do not erase it.
Heads-up Spawning threads does not serialise them — on multiple cores they run in parallel and can interleave freely. Taking turns through the shared update is exactly what is missing here; that is what a lock would add.
Snippet 4 — adding a lock
total = 0lock = new Lock()each of 1000 threads does: lock.acquire() total = total + 1 // protected: only one thread in here at a time lock.release()
Quiz
Completed
What does the lock change, and what is the cost?
Heads-up A lock does the opposite of 'all at once' — it forces threads through the protected section one at a time. That is how it removes the race, but it also removes parallelism for that section, so it is a cost, not a speedup.
Heads-up While one thread holds the lock, no other can enter the protected section, so the read-add-write cannot interleave. The race is gone — the result is exactly 1000. The lock is precisely what serialises the shared update.
Heads-up The threads still run and still do the work; the lock only controls when they may enter the shared section. It coordinates the threads, it does not replace them.
Recap
Reading concurrency means reading when things run. The event loop puts synchronous code first and callbacks last (start, end, timeout). When work is split across threads that share state, a plain total = total + 1 is really read-add-write, so parallel threads can interleave, read the same value, and overwrite each other — a data race that loses updates and gives a wrong, nondeterministic total. A lock fixes it by making the shared section indivisible: one thread at a time, exact result restored. The cost is that the locked section runs serially. The senior move is to keep the lock for correctness and make the locked region as small as possible.