Basic

Two writers, one value

A race condition is a result that depends on timing. Two writers that read then write the same value can lose one of the writes, and nothing in the code says so.

What you will see

True

About 2 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

Check the shape of a race.

1

Two steps shown.

total = 0 def add(n): global total current = total current = current + n total = current add(1) add(1) print(total)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
2
2

Same value twice.

total = 0 a = total b = total a = a + 1 b = b + 1 total = b print(total)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
1
3

Single writer.

total = 0 for _ in range(3): total = total + 1 print(total)

Before you run it, say out loud what it will print. Then open the answer.

What it prints
3
4

Range asserted.

values = [1998, 2000, 1999] print(all(1990 <= v <= 2000 for v in values))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
True

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

Two writers read the same value, the window between read and write lets one overwrite the other, and the total comes up short.
1 writer A reads
↓
2 writer B reads
↓
3 A writes
↓
4 B writes over it
Line by line

Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.

total = 0 sets nothing on screen
def add(n): sets nothing on screen
global total sets nothing on screen
current = total sets nothing on screen
current = current + n sets nothing on screen
total = current sets nothing on screen
add(1) sets nothing on screen
add(1) sets nothing on screen
print(total) 2
Why this works
The window is the bug

Nothing is wrong with the read or the write. The bug is the gap between them, which is why the fix is about order and not about code.

Tests hide it

A race often passes on a fast machine and fails on a slow one. Assert a range, or force the overlap with a sleep, before believing a green run.

Examples

Read then write

total = 0

def add(n):
    global total
    current = total
    current = current + n
    total = current

add(1)
add(1)
print(total)

Two steps: read, then write. Between them anything else can write, and one of the writes is lost.

The window

total = 0
# writer A reads total, sees 0
# writer B reads total, sees 0
# A writes 1, B writes 1
print(total)

Both read the same value, so both write the same value. One increment is gone and no error is raised.

Threads make it real

import threading

total = 0

def add():
    global total
    for _ in range(1000):
        total = total + 1

a = threading.Thread(target=add)
b = threading.Thread(target=add)
a.start()
b.start()
a.join()
b.join()
print(total >= 2000)

The count can land anywhere up to 2000. A test that asserts 2000 here fails at random.

A sign you can see

import random
value = random.choice([1998, 2000, 1999])
print(value == 2000)

A number that changes between runs is the symptom. The cause is two writers with no order between them.

One writer is safe

total = 0
for _ in range(3):
    total = total + 1
print(total)

A single writer in a single task has no window. The race needs two writers, which is why it hides in tests.

Name the shared value

shared = {"count": 0}
local = shared["count"] + 1
print(local)

A value that more than one task can reach is shared. Naming it in a comment is what makes the risk reviewable.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Order is not finished order
Previous Lesson
Order is not finished order
Next Lesson
A lock keeps one inside
A lock keeps one inside →