Base CS from zero
Time and concurrency: reproduce and fix a race
Reading about race conditions is not the same as watching one eat your data. Build the smallest program that exhibits a race — many threads incrementing one shared counter — make the wrong number appear with your own eyes, then fix it with a lock and prove the fix with counts, not faith.
Turn the unit’s ideas into something you can run: structure work into threads (concurrency), let them touch shared state in parallel, reproduce a lost-update race, explain it as a read-add-write interleaving, then add a lock and verify the count is correct and stable across many runs.
Write a tiny multi-threaded program that increments one shared counter from many threads, reproduce a lost-update race condition, then fix it with a lock — proving the bug and the fix with measured before/after counts, not estimates.
- A before/after table: 10 unsynchronized runs (showing varying, too-low counts) next to 10 locked runs (all equal to the exact expected total).
- The expected total is stated explicitly (threads x iterations per thread) so the gap in the unsynchronized runs is unambiguous.
- A one-paragraph explanation of WHY the unsynchronized version loses updates, in terms of read-add-write interleaving — not just 'threads are unsafe'.
- A one-sentence statement of what the lock changed: it makes the increment indivisible, so only one thread is inside the protected section at a time.
- Measure wall-clock time for both versions and report it next to the counts. Explain why the locked version can be slower: the protected section now runs serially, so threads queue for the lock instead of running in parallel.
- Shrink the contention: have each thread keep a private local count and add it to the shared counter once at the end under the lock. Show the result is still correct but faster, and explain why (far less time spent holding the lock).
- Replace the lock with an atomic-increment primitive if your language offers one (e.g. atomic types, Go's sync/atomic, Java's AtomicInteger). Show it is correct and explain how it makes read-add-write a single indivisible step without an explicit lock.
- Add a second shared variable and a deliberately wrong fix (two separate locks, or releasing too early) to see the race reappear, demonstrating that a lock only protects what it actually guards.
This is the loop you will run whenever concurrency meets shared state: structure the work into threads, let them touch the same data, and watch a race condition produce a wrong, run-to-run-varying answer because counter = counter + 1 is really read-add-write and the threads interleave. Then add a lock to make the shared section indivisible, and prove with counts that the result is now exactly right every time. Doing it once on a toy counter makes the production version — where the shared state is a balance, an inventory, or a session — something you can reason about instead of fear.