Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
The interleaved output of two unsynchronized printers is your first tangible proof that concurrent execution is non-deterministic. The same program produces different output on different runs — not because of bugs, but because the OS scheduler can context-switch between any two instructions. Once you see output like A 0 A 1 B 0 A 2 B 1 and understand that neither order is 'correct', you start to understand why shared mutable state needs explicit synchronization. This small program is a microcosm of the bigger problem: any time two tasks share state without coordination, the output depends on timing that neither your code nor your tests can predict.
Tabs below show the same program in Node.js, Go, Python, and Rust. Each spawns two tasks that print "A" and "B". Depending on the scheduler you'll sometimes see AB, sometimes BA, sometimes garbled output.
Use these three in order. Each builds on the one before.
Describe what a 'task' is in Go, Python, Rust, and Node.js, and how I spawn one in each.
Why is the output of two unsynchronized concurrent printers non-deterministic? What exactly in the runtime or OS produces the variance?
Modify the program so output is strictly alternating (A then B then A then B…) without using `sleep`. Compare approaches: mutex, semaphore, channel, condition variable.
// main.js — two async tasks race to print
async function speak(label) {
for (let i = 0; i < 5; i++) {
console.log(label, i);
await new Promise((r) => setTimeout(r, 0));
}
}
await Promise.all([speak("A"), speak("B")]);node main.js