Threads, Locks & the Anatomy of a Race

Share memory between two threads and the number of ways their steps can interleave explodes — most correct, a few catastrophic. Walk the canonical lost update, scrub an adversarial schedule op by op, then hold the invariant with a mutex, condition variables done right, and a deadlock you can watch form.

Concept · Systems. The source ↗

A free, interactive, animated visual explainer of Threads, Locks & the Anatomy of a Race — built to be understood, not skimmed.

Questions

What is a race condition?
A race condition is a bug where the result of a program depends on the nondeterministic order in which concurrent threads interleave their steps. It shows up when two operations on shared data overlap: Oracle’s docs define it as when "two operations, running in different threads, but acting on the same data, interleave" — because each operation is really several machine steps (a read, a modify, a store), one thread’s update can be silently overwritten by another’s. The canonical example is two threads each incrementing the same counter and the total coming out as one instead of two.
How does a mutex fix a race condition?
A mutex (mutual-exclusion lock) makes a critical section — the read-modify-write that must not be interrupted — run atomically with respect to other threads. Only the thread holding the lock may execute inside the section; everyone else waits. The interleaving that lost the update can no longer happen because the second thread cannot read the shared value until the first has finished writing it. The Go memory model states the underlying rule plainly: "Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access."
Why must you wait on a condition variable in a while loop, not an if?
Because a wakeup does not prove the thing you waited for is true. A thread can suffer a spurious wakeup — Java’s docs note "a thread can wake up without being notified, interrupted, or timing out, a so-called spurious wakeup" — and even a real signal can be stolen by another thread that runs first and consumes the resource. With if, the woken thread proceeds on a false assumption and corrupts state; with while, it re-tests the predicate and simply waits again. The rule is absolute: "a Condition should always be waited upon in a loop, testing the state predicate that is being waited for."
What causes a deadlock and how do you prevent it?
A deadlock is when "two or more threads are blocked forever, waiting for each other" — each holds a lock the other needs, forming a cycle in the wait-for graph. The classic trigger is two threads acquiring the same two locks in opposite orders. The standard prevention is a global lock ordering: every thread acquires locks in the same agreed order, which makes a cycle impossible. Other approaches are lock timeouts, try-lock-and-back-off, or acquiring all locks atomically.
What is a memory visibility bug and what is happens-before?
Even with no torn read, one thread’s write may simply not be visible to another — the value sits in a register or a core’s cache, or the compiler reordered instructions. A memory consistency error "occurs when different threads have inconsistent views of what should be the same data." The tool that rules it out is the happens-before relationship, "a guarantee that memory writes by one specific statement are visible to another specific statement." Locks, atomics, and channels all establish happens-before edges; without one, "it worked on my machine" is just the race not having lost yet.

Related explainers