Skip to content

Mindcomb · Coding

Spot the race condition

Curated by Mindcomb

Prompt

Two threads call withdraw(80) at the same time on this Python code. What can go wrong, how likely is it, and how would you fix it? Show the fixed code. import time balance = 100 def withdraw(amount): global balance if balance >= amount: time.sleep(0.01) balance -= amount return True return False

Mindcomb ran and reviewed this comparison for the public library. It was not saved by a community member.

Prompt source
Mindcomb
License
Mindcomb original
Retrieved
Fork this combTry this promptNo winner picked · recorded cost $0.05678
Compare answers word by word

Community preferences

Which answer would you choose? Your preference is separate from the author's pick and appreciation.

Loading community preferences…

Claude Sonnet 5.5

What goes wrong

This is a classic check-then-act race condition (TOCTOU: time-of-check to time-of-use).

Interleaving that breaks it:

StepThread AThread Bbalance
1balance >= 80 → True100
2sleep(0.01) (yields GIL)balance >= 80 → True100
3sleep(0.01)100
4balance -= 8020
5returns Truebalance -= 80-60
6returns True-60

Both withdrawals succeed, and the account ends at -60 even though only 100 was available. You've effectively dispensed 160 from a 100 balance.

A second, subtler problem: balance -= amount is not atomic in Python. It compiles to load, subtract, store. Even without the sleep, two threads could both load 100 and both store 20, losing an update. (In that case you'd get 20 rather than -60, which is a different bug: money is dispensed twice but only deducted once.)

How likely is it?

Very close to certain as written. The time.sleep(0.01) sits right between the check and the update, and sleep releases the GIL and forces a thread switch. If both threads start at roughly the same time, Thread B will almost always run its check during Thread A's sleep. The 10 ms window is enormous compared to the microseconds a thread needs to run the check.

Without the sleep, the window shrinks to a few bytecodes, so it would be rare (the GIL switches every ~5 ms by default, or on blocking I/O), but it would still be possible, and rare races are the worst kind because they pass tests and fail in production. The sleep here just stands in for real work such as a database call or network request, which is exactly what happens in real code.

The GIL does not protect you. It makes individual bytecodes safe, not multi-step logic.

The fix

Make the check and the update a single atomic critical section with a lock:

import threading
import time

balance = 100
balance_lock = threading.Lock()

def withdraw(amount):
    global balance
    with balance_lock:
        if balance >= amount:
            time.sleep(0.01)  # simulated work, now safely inside the lock
            balance -= amount
            return True
        return False

Now one thread does check → sleep → update completely before the other can even check. The second thread then sees balance == 20, and withdraw(80) returns False.

Verification

if __name__ == "__main__":
    results = []
    threads = [
        threading.Thread(target=lambda: results.append(withdraw(80)))
        for _ in range(2)
    ]
    for t in threads: t.start()
    for t in threads: t.join()

    print(results)   # [True, False] (order may vary)
    print(balance)   # 20

Better design notes

  • Keep the lock scope tight. Holding a lock during sleep or I/O serializes everything. Here it's necessary, because the work is part of the transaction. If the slow part didn't depend on the balance, move it outside the lock.
  • Encapsulate state and lock together rather than using globals, so no one can touch balance without the lock:
class Account:
    def __init__(self, balance):
        self._balance = balance
        self._lock = threading.Lock()

    def withdraw(self, amount):
        with self._lock:
            if self._balance >= amount:
                time.sleep(0.01)
                self._balance -= amount
                return True
            return False
  • Across processes or machines, a threading.Lock doesn't help. Use database transactions (e.g. UPDATE accounts SET balance = balance - 80 WHERE id = ? AND balance >= 80 and check the affected row count), which does the check and update atomically in one statement.

Server-recorded answer

Model: claude-sonnet-5-5

10.3s · 1,440 tok · $0.01343

Gemini 4 Argon

1. What can go wrong?

This code contains a classic race condition (specifically, a Time-of-Check to Time-of-Use, or TOCTOU, bug).

If two threads call withdraw(80) simultaneously, both withdrawals will succeed, True will be returned to both threads, and the final balance will be -60 instead of 20.

Here is the step-by-step execution of how it fails:

  1. Thread A checks if balance >= 80. Since balance is 100, this evaluates to True.
  2. Thread A hits time.sleep(0.01). Python suspends Thread A and hands control over to Thread B.
  3. Thread B checks if balance >= 80. Because Thread A hasn't subtracted the money yet, balance is still 100. This evaluates to True.
  4. Thread B hits time.sleep(0.01) and goes to sleep.
  5. Thread A wakes up, calculates 100 - 80, and sets balance to 20. It returns True.
  6. Thread B wakes up, calculates 20 - 80, and sets balance to -60. It returns True.

2. How likely is it?

With the time.sleep(0.01) included, it is virtually 100% guaranteed to happen.

Even though Python has a Global Interpreter Lock (GIL) that prevents two threads from executing Python bytecode at the exact same nanosecond, blocking I/O operations (like time.sleep(), network requests, or database queries) automatically release the GIL.

The moment Thread A hits time.sleep(), it willingly gives up the CPU, practically guaranteeing that Thread B will run and read the stale balance before Thread A wakes up.

(Note: Without time.sleep(), this bug would be very rare in pure Python because the GIL would likely allow the if check and the subtraction to finish before switching threads, though it is still theoretically possible and unsafe).

3. How to fix it

To fix this, you need to make the check-and-update operation atomic (indivisible). You can do this using a Mutex Lock (threading.Lock).

When a thread acquires a lock, any other thread that tries to acquire the same lock will be forced to wait until the first thread releases it. Even if the first thread goes to sleep, it holds onto the lock, keeping the other thread out.

The Fixed Code

We use Python's threading.Lock() and the with statement (context manager), which automatically acquires the lock when entering the block and safely releases it when exiting.

import threading
import time

balance = 100
# 1. Create a lock object
balance_lock = threading.Lock()


def withdraw(amount):
    global balance

    # 2. Acquire the lock before checking the balance
    with balance_lock:
        if balance >= amount:
            time.sleep(0.01)  # Other threads must still wait while this sleeps
            balance -= amount
            return True
        return False


# --- Testing the fix ---
def make_withdrawal():
    success = withdraw(80)
    print(f"Withdrawal of 80 successful? {success} | Remaining: {balance}")


# Create two threads running at the same time
t1 = threading.Thread(target=make_withdrawal)
t2 = threading.Thread(target=make_withdrawal)

t1.start()
t2.start()

t1.join()
t2.join()

Output of the fixed code:

Withdrawal of 80 successful? True | Remaining: 20
Withdrawal of 80 successful? False | Remaining: 20

Server-recorded answer

Model: gemini-4-argon

14.3s · 1,007 tok · $0.009102

GPT-5.5

What can go wrong: race condition.

Both threads can read balance == 100 before either one subtracts:

Thread A: sees balance 100, passes check
Thread B: sees balance 100, passes check
Thread A: subtracts 80 -> balance 20
Thread B: subtracts 80 -> balance -60

So both withdrawals may return True, even though only one should succeed. The final balance can become negative, e.g. -60.

The time.sleep(0.01) makes this very likely because it gives the other thread time to run between the check and the update. The Python GIL does not fix this because the whole “check then subtract” operation is not atomic.

Fix it with a lock so the check and update happen as one critical section:

import threading
import time

balance = 100
balance_lock = threading.Lock()

def withdraw(amount):
    global balance

    with balance_lock:
        if balance >= amount:
            time.sleep(0.01)  # only here to mimic delay; usually avoid sleeping inside locks
            balance -= amount
            return True

        return False

Now only one thread can execute the balance check/update at a time. With two simultaneous withdraw(80) calls:

One returns True, balance becomes 20.
The other returns False, balance stays 20.

A slightly better version avoids sleeping while holding the lock if the sleep is just simulating unrelated work:

import threading

balance = 100
balance_lock = threading.Lock()

def withdraw(amount):
    global balance

    with balance_lock:
        if balance < amount:
            return False

        balance -= amount
        return True

In real financial systems, this should usually be handled with database transactions, row locks, or atomic updates rather than an in-memory Python variable.

Server-recorded answer

Model: gpt-5.5-2026-04-23

13.8s · 1,007 tok · $0.02806

Grok 4.7

Both threads can pass the balance check before either subtracts, so both withdrawals succeed and the balance goes negative. With the sleep between the check and the update, that is very likely.

balance >= amount and balance -= amount are separate steps. Nothing stops another thread from running between them. time.sleep releases the GIL, so both threads typically do this:

  1. Thread A sees balance == 100 and passes the check.
  2. Thread B sees balance == 100 and passes the check.
  3. Both subtract 80.
  4. Both return True. Final balance is -60 instead of 20, and 160 was withdrawn from 100.

Even without the sleep, the check and the update are not one atomic operation, so the race is still possible. The sleep just makes it easy to hit.

Hold a lock across the whole check-and-update so only one thread can do it at a time:

import threading

balance = 100
balance_lock = threading.Lock()

def withdraw(amount):
    global balance
    with balance_lock:
        if balance >= amount:
            balance -= amount
            return True
        return False

The lock makes the critical section atomic: the second thread waits until the first has finished, then sees balance == 20 and returns False. Drop the sleep; it was only widening the race. Do not read or write balance anywhere else without the same lock.

Server-recorded answer

Model: grok-4.7

8.1s · 1,914 tok · $0.006184