books / clean-code / ch-10SHEET 10 / 12 · REV ASIGN IN
MODULE 10 · CLEAN CODE CH 13 + APP A

Clean Concurrency

A race condition you can trigger on demand is just a bug; one you can't is a superstition.

The idea

Every other bug in this track fails the same way every time. A race condition breaks that contract. The program is correct a thousand runs in a row, then wrong once, because the operating system may pause any thread at any instruction. Two threads read a counter that says 41, both add one, both write 42, and one update simply vanishes without a crash.

So the first job is not fixing, it is reproducing. Run with far more threads than you have cores so the scheduler is forced to interleave. Loop the hot path thousands of times so a one-in-a-million ordering becomes a certainty. Insert tiny sleeps between the read and the write to shake loose the orderings the scheduler was hiding.

Every race needs two ingredients: shared data, and more than one thread mutating it. Remove either and it is gone. Keep the sharing and guard the mutation with a lock. Keep the threads and remove the mutation by giving each worker a private result that one final step merges. Or keep the mutation and remove the sharing by confining the data to a single writer that everyone else messages through a queue.

None of those is free, so measure instead of guessing. Locks serialize the hot path and invite deadlock the moment there are two of them, which lock ordering cures. Copy-and-merge spends memory. A queue adds latency and moving parts. The real lesson is to keep the concurrent part tiny, know what it costs, and keep it away from everything else.

The bench — 4 exercises

EX 01

The Race Safari

Take a parallel tally that is usually right and turn it into a failure you can produce on demand. Converting flaky into reproducible is the skill this whole module rests on.

  1. Write a small program where N threads each increment a shared total M times, with a deliberate gap between reading the value and writing it back
  2. Run it once with a few threads and note that the total is often correct
  3. Oversubscribe: raise the thread count well above your core count, then raise iterations until the total goes wrong
  4. Add one microsleep between the read and the write and re-run the whole thing ten times, recording how many runs lost updates
  5. Save the parameters that fail reliably as your stress test for every fix that follows
hint

Failing only two or three times in ten? Add iterations before adding threads. The race needs traffic through the read-modify-write gap more than it needs contenders.

hint

In Python, split the increment across statements so the GIL cannot hide it. In Node, use worker_threads with a SharedArrayBuffer, since a single event loop cannot race on a plain object.

DONE WHEN

· Ten consecutive runs, at least nine show a final total below the expected value

· The same stress parameters yield the correct total every time once the bug is fixed

· The failing configuration is written down and re-runnable without editing code

EX 02

Three Fixes for One Bug

Fix the identical race three different ways and benchmark them. Knowing the whole menu, not just the lock, is the point.

  1. Fix one: guard the shared tally with a mutex, keeping the critical section to a handful of lines
  2. Fix two: give each worker a private accumulator and merge them into a final result after all workers join, mutating nothing shared
  3. Fix three: let workers push increment messages onto a queue consumed by a single writer thread that owns the tally outright
  4. Run your exercise-one stress config against all three and record wall time and peak memory in a small table
  5. Write two sentences under the table naming which fix you would ship for this workload and why
hint

On the immutable version, resist locking the merge. The merge runs after the workers join; if you feel you need a lock there, a worker is still alive.

hint

Compare diffs at the end. Small functions and encapsulation from earlier modules should make each fix startlingly short.

DONE WHEN

· All three versions pass the stress config ten times out of ten

· The single-threaded behavior of the program is unchanged in all three

· A benchmark table plus a written verdict exists in your notes

EX 03

The Deadlock Lab

Freeze your own program on purpose with two locks taken in opposite orders, then unfreeze it with a global lock order rather than by deleting a lock.

  1. Add a second lock (say an audit log) and write two code paths that acquire the two locks in opposite orders
  2. Run under stress until the program hangs, and add a watchdog that kills it after ten seconds and prints which lock each thread holds and waits on
  3. Read the dump and write the cycle out in one line: A holds X wants Y, B holds Y wants X
  4. Impose one global acquisition order across every site that takes both locks, changing no lock away
  5. Re-run the stress ten times and confirm no hang and correct totals
hint

Do not hunt the interleaving, hunt the order. List every site that takes both locks and the sequence it uses; the bug is the line that disagrees.

hint

Deadlock needs four conditions at once; breaking circular wait is the one you will use weekly.

DONE WHEN

· The unfixed version reliably hangs and the watchdog prints a readable held/waiting dump

· Ten stress runs after the fix finish with no timeouts and correct totals

· Both locks are still present in the code

EX 04

Bounded Queue, Clean Shutdown

Build a fixed-capacity queue by hand connecting a parser stage to a writer stage, and stop it cleanly with a poison pill. Stopping is the hard part of concurrent design.

  1. Implement a queue with fixed capacity using condition variables or events, not an off-the-shelf concurrent queue class
  2. Make producers block when the queue is full and consumers block when it is empty
  3. Wire four producers and two consumers at capacity eight over your app's parse-then-write path
  4. Shut down by enqueuing sentinel values, making sure every consumer exits rather than hanging
  5. Run the pipeline twenty times and diff input records against output records
hint

Multiple consumers and one poison pill means one consumer exits and the rest hang. Enqueue one pill per consumer, or have each consumer re-enqueue before exiting.

hint

Record the queue's high-water mark to prove capacity was actually enforced; that is what backpressure looks like.

DONE WHEN

· Across twenty runs every input record appears in the output exactly once

· No run hangs on shutdown or exceeds the watchdog timeout

· The observed queue length never exceeds the configured capacity

Go deeper (after the bench)

Read Clean Code ch. 13 and then Appendix A, Concurrency II. The appendix is the better half: its instrumented-test walkthroughs and client/server lock discussion go well past the main chapter. For free practice, play The Deadlock Empire (deadlockempire.github.io), a browser game where you play the malicious scheduler and step threads by hand to break synchronized code.