A logbook for curious waters
← Software Engineering Field Guide

Race Conditions and Critical Sections

Before you begin: Shared variables, threads, assignment, and locks.

A race condition makes correctness depend on the relative timing of operations. A critical section is work that must not interleave with competing work on the same protected state. A lock is one way to enforce that exclusion. 1

A familiar example is two workers incrementing one counter. “Add one” can involve reading the old value, calculating a new value, and writing it back. Protecting only the write does not make that entire sequence indivisible.

Original problem: two increments become one

Start with a counter of zero. One possible ordering is:

StepWorker AWorker BStored counter
1Reads zero0
2Reads zero0
3Writes zero plus one1
4Writes zero plus one1

Both workers performed their intended arithmetic, but one update was lost. We can reproduce this ordering directly without hoping a thread scheduler happens to choose it.

public static class LostUpdate
{
    public static int Simulate()
    {
        int counter = 0;
        int readByA = counter;
        int readByB = counter;
        counter = readByA + 1;
        counter = readByB + 1;
        return counter;
    }
}

This is an execution model of the table, not a multithreaded stress test. Its result is deterministically one, showing why that interleaving would be wrong.

def simulate_lost_update():
    counter = 0
    read_by_a = counter
    read_by_b = counter
    counter = read_by_a + 1
    counter = read_by_b + 1
    return counter

The function deliberately spells out the interleaving. It does not claim that every Python interpreter executes an ordinary increment as these separately scheduled steps.

Corrected version: protect the complete update

public sealed class Counter
{
    private readonly object gate = new object();
    private int value;

    public void Increment()
    {
        lock (gate) { value = value + 1; }
    }

    public int Read()
    {
        lock (gate) { return value; }
    }

    public static async Task<int> CheckConcurrentIncrements()
    {
        var counter = new Counter();
        Task first = Task.Run(() => { for (int i = 0; i < 1000; i++) counter.Increment(); });
        Task second = Task.Run(() => { for (int i = 0; i < 1000; i++) counter.Increment(); });
        await Task.WhenAll(first, second);
        return counter.Read();
    }
}

Both workers acquire the same gate. One cannot read the protected value in the middle of the other’s increment. The test waits for both workers before expecting two thousand.

from threading import Lock, Thread


class Counter:
    def __init__(self):
        self._gate = Lock()
        self._value = 0

    def increment(self):
        with self._gate:
            self._value += 1

    def read(self):
        with self._gate:
            return self._value


def check_concurrent_increments():
    counter = Counter()

    def work():
        for _ in range(1000):
            counter.increment()

    workers = [Thread(target=work), Thread(target=work)]
    for worker in workers:
        worker.start()
    for worker in workers:
        worker.join()
    return counter.read()

The explicit lock protects the application’s intended update regardless of assumptions about interpreter scheduling. Joining both threads establishes that their increments have completed before the final read.

Protect a rule, not just a field

For an inventory operation, “check that stock is available, then subtract” may need to be one critical section. Individually safe reads and writes can still permit two buyers to reserve the last item.

An atomic increment such as C# Interlocked.Increment is a simpler choice for some counters. It does not automatically protect a larger multi-field rule. A process-local lock also does not coordinate other processes writing to the same database; that needs database-level concurrency control.

Keep critical sections focused, ensure every relevant access follows the same policy, and avoid blocking external calls while holding a lock. Locks can introduce waiting and deadlock if acquired inconsistently. They solve an interleaving problem only when their scope matches the operation that needs protection.

Sources

  1. Microsoft — The lock statement, for mutual exclusion and structured release.
  2. Python — Lock objects.