AW Dev Rethought

✂️ Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away - Antoine de Saint-Exupéry

⚙️ Python Systems Engineering — 🔀 Race Conditions 🐍


Description:

Two workers each add one to a counter starting at zero. The expected result is two. Yet the counter can finish at one—even though each worker calculated its addition correctly.

The problem is how their operations overlap. This lab makes that overlap explicit, then fixes it with a shared lock.


The lost update

An increment involves reading a value, calculating a new value, and writing it back. Consider this order:

Step Worker A Worker B Counter
1 Reads 0 0
2 Reads 0 0
3 Calculates 1 Calculates 1 0
4 Writes 1 1
5 Writes 1 1

Worker B overwrites the counter with a value calculated from the same old state. One update is lost. This is a race condition: correctness depends on the ordering of concurrent operations.


Reproduce it deliberately

The accompanying notebook uses only Python's standard library:

from concurrent.futures import ThreadPoolExecutor
from threading import Barrier, Lock

A Barrier(2, timeout=5) waits for both workers to arrive. Placing it after the read ensures both workers capture zero before either can write. The worker's critical lines are:

snapshot = counter
both_read.wait()
counter = snapshot + 1

The notebook runs two workers and waits for both results. The output is:

Values read: [0, 0]
Expected: 2 | Actual: 1

This is an intentionally arranged interleaving, not a benchmark or an estimate of race frequency. We do not depend on a short expression such as counter += 1 happening to fail on a particular interpreter build.


Protect the complete operation

Create one shared lock before starting the workers. Each worker acquires it before reading the counter and releases it after writing:

ready.wait()  # Both workers start; keep this outside the lock.
with lock:
    snapshot = counter
    counter = snapshot + 1

Whichever worker acquires the lock first reads zero and writes one. The other then reads one and writes two. Worker identities are not ordered; the operations are.

Values read (sorted): [0, 1]
Expected: 2 | Actual: 2

The notebook checks both versions across 20 fresh runs each. These checks verify this controlled example, not every possible concurrent program.


Three details that matter

  • Lock the read and the write together. Locking only the write still allows both workers to calculate from stale reads.
  • Use the same lock for competing access. A separate lock per worker does not coordinate them.
  • Keep the two-party barrier outside the lock. Otherwise the first worker waits while preventing the second from entering. In this example the barrier timeout exposes that mistake instead of waiting forever.

The GIL is not an application-level transaction around arbitrary multi-step updates. Explicit coordination expresses the correctness rule directly. Also, threading.Lock coordinates threads in one process; shared database or cross-server updates require coordination at that boundary.


Run the lab

Open Race_Conditions_Lost_Update.ipynb in Jupyter and run the cells from top to bottom. No extra packages are needed by the example itself. A commented standalone version is also included:

python race_conditions_demo.py

Takeaway: correct individual steps do not guarantee a correct combined result. Identify the whole operation that must stay consistent, then coordinate every competing access to it.

Code Snippet:

None

Link copied!

Comments

Add Your Comment

Comment Added!