liveBook · Manning

Race condition

As explained in Python Concurrency with asyncio

A race condition is a concurrency bug in which the result or observable behavior depends on the timing or ordering of concurrent operations.

A race condition is a concurrency bug in which the outcome depends on which concurrent operation finishes first. Shared data can therefore end in an unexpected or inconsistent state. section 6.5.1 section 1.5

The usual trigger is a non-atomic operation that looks like one action but contains several steps. Incrementing a counter, for example, reads its value, adds one, and writes the result; two workers can both read zero and overwrite each other with one. section 6.5.1 section 11.1

Two processes incrementing one counter

Two processes each increment a shared integer and the program expects two. Repeated runs sometimes produce one, demonstrating nondeterministic behavior caused by overlapping reads and writes. section 6.5.1

Recognize the pattern across Python concurrency models:

  • Threads can race when they modify shared, non-thread-safe data because execution order is uncontrolled. section 7.3
  • Processes can race when they use explicitly shared memory, even though ordinary process memory is separate. section 7.3 section 6.5.1
  • Asyncio avoids races from ordinary non-atomic operations while one coroutine runs continuously, but an await can allow another task to observe or change shared state before the operation finishes. section 11.1

A single-threaded asyncio race

An increment coroutine copies the counter to a temporary variable, awaits briefly, and writes the temporary value back. Many tasks can consequently lose updates even though only one thread executes Python code at a time. section 11.1

Listing 11.2 A single-threaded race condition
import asyncio
 
counter: int = 0
 
 
async def increment():
    global counter
    temp_counter = counter
    temp_counter = temp_counter + 1
    await asyncio.sleep(0.01)
    counter = temp_counter
 
 
async def main():
    global counter
    for _ in range(1000):
        tasks = [asyncio.create_task(increment()) for _ in range(100)]
        await asyncio.gather(*tasks)
        print(f'Counter is {counter}')
        assert counter == 100
        counter = 0
 
 
asyncio.run(main())

The runnable asyncio example makes the suspension point and lost updates visible in a compact counter test. section 11.1

Protect a critical section with a lock so only one worker changes shared state at a time. Threaded code uses threading.Lock; multiprocessing and asyncio use their corresponding synchronization primitives. Keep the protected region focused, since locks can also contribute to deadlocks. section 7.3 section 6.7

Counting completed requests

A thread-pool program makes many blocking web requests and increments a shared completion counter inside a threading.Lock context, allowing a reporter task to display progress safely. section 7.3

How Python Concurrency with asyncio covers it

How deep it goes
Explains timing, non-atomic read-modify-write steps, execution-model differences, and lock-based prevention; it does not develop synchronization internals.
Examples
Demonstrates races with reference counts, shared counters, request progress, socket dictionaries, and suspended asyncio tasks.
What it assumes
Assumes basic Python functions and shared variables while teaching concurrency hazards and synchronization from concrete counter examples.
What you can do after
Diagnose/debug timing-dependent shared-state bugs and protect critical sections with the appropriate Python lock.
How it connects
Connects race conditions to concurrency, threads, processes, asyncio, shared memory, locks, critical sections, and deadlocks.

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 Python Concurrency with asyncio. The section links point to the parts of the book it draws on.