liveBook · Manning

Race condition

As explained in Grokking Concurrency

A race condition is a concurrency bug in which the result depends on the timing or ordering of competing accesses to shared state or resources.

Imagine two ATMs depositing $10 into an account with a $0 balance. Both read $0, both calculate $10, and both save $10. One deposit disappears. A race condition occurs when concurrent tasks access shared state and the correct result depends on which task gets there first. section 8.2

ATM A deposit()

ATM B deposit()

Balance

Get balance

←

0

Get balance

←

0

Add 10

Add 10

Write back the result

→

10

Write back the result

→

10

Follow the two deposits step by step: each ATM reads the old balance before either writes its result. section 8.2

A deposit may look like one operation, but it reads a balance, adds an amount, and writes the result. Threads can switch between those steps, whether they run on different processor cores or take turns under the OS scheduler. Compiler and hardware optimizations can also affect when memory operations become visible. That makes race-condition errors difficult to reproduce. section 8.2

An unreliable balance

In a Python test, many ATM threads each deposit $10 and withdraw $10 from one unsynchronized account. The expected final balance is $0; one run instead produces $380. The incorrect amount is a symptom, not a result to expect on every run. section 8.2

# Chapter 8/race_condition/synced_bank_account.py
from threading import Lock
from unsynced_bank_account import UnsyncedBankAccount

class SyncedBankAccount(UnsyncedBankAccount):
    def __init__(self, balance: float = 0):
        super().__init__(balance)
        self.mutex = Lock()

    def deposit(self, amount: float) -> None:
        self.mutex.acquire()                            #1
        super().deposit(amount)
        self.mutex.release()                            #2

    def withdraw(self, amount: float) -> None:
        self.mutex.acquire()                            #1
        super().withdraw(amount)
        self.mutex.release()                            #2

The synchronized account puts both deposit and withdrawal behind the same Python Lock, so a thread holds the mutex while it accesses the balance. section 8.4

Protect every critical section that accesses the shared balance with the same mutex; a thread trying to acquire an occupied lock waits. Other approaches include atomic operations and message passing. Synchronization has a cost, so avoiding shared mutable resources where possible is also worth considering. section 8.4 section 8.7

How Grokking Concurrency covers it

How deep it goes
Works through interleaved balance updates and mutex-protected methods, while treating compiler and hardware reordering conceptually.
Examples
Contrasts interleaved ATM deposits, an unexpected balance from Python threads, and a mutex-protected account.
What it assumes
Introduces concurrency through account transactions; its implementation uses basic Python methods and threads.
What you can do after
Diagnose/debug with it by tracing competing reads and writes, then protect shared balance updates with a common mutex.
How it connects
Connects shared state and OS scheduling to critical sections, mutexes, atomic operations, and message passing.

Also explained in

Compare how 20 books explain race condition →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Grokking Concurrency. The section links point to the parts of the book it draws on.