1 + 1 = 1
Two threads share memory. Each runs counter++, which is really three steps: load, add, store. If the scheduler switches threads in the middle, one update can overwrite the other — a race condition. Such bugs appear only sometimes, which makes them nasty.
// Each thread runs counter++ as three machine steps.
function runSchedule(schedule) {
let counter = 0;
const reg = { A: 0, B: 0 };
const pc = { A: 0, B: 0 };
for (const t of schedule) {
if (pc[t] === 0) reg[t] = counter; // load
else if (pc[t] === 1) reg[t] = reg[t] + 1; // add
else if (pc[t] === 2) counter = reg[t]; // store
pc[t]++;
}
return counter;
}
function allSchedules(a, b) {
if (a === 0) return ["B".repeat(b)];
if (b === 0) return ["A".repeat(a)];
return [...allSchedules(a - 1, b).map((s) => "A" + s), ...allSchedules(a, b - 1).map((s) => "B" + s)];
}
function outcomes() {
return [...new Set(allSchedules(3, 3).map(runSchedule))].sort();
}Counting interleavings
Race conditions are among the hardest bugs in software: they hide in testing, appear under load and vanish when you look closely. Understanding exactly how updates get lost is the first step to preventing them.
Two threads with three steps each can be interleaved in 20 different orders (choose which 3 of the 6 slots belong to thread A). Some orders run one increment completely before the other and give 2. Others let both threads load 0 before either stores, and the counter ends at 1. Trying every interleaving is how model checkers find these bugs reliably.
// A: load add store B: load add store // "AAABBB" → A finishes first → 2 // "ABABAB" → both load 0, both store 1 → 1 // "AABBBA" → A loads 0, B runs fully (1), // A stores its stale 1 → 1