Skip to the solution
MediumDSA7 min read

What is a race condition and how do locks prevent it?

1.2k views

The problem

Question presented to candidate: "What is a race condition? Show me one in Python β€” and explain why the GIL does not save you, and how a lock fixes it."

What a strong answer should cover:

  • A race condition: the result depends on the timing or interleaving of concurrent tasks that share state; classic forms are lost updates (read-modify-write) and check-then-act.
  • counter += 1 is several bytecode steps (load, add, store); a thread switch between the load and the store loses an update. The GIL makes single bytecodes safe for the interpreter, not your multi-step operation.
  • A lock (threading.Lock) makes a critical section mutually exclusive: only one thread at a time runs it, so read-modify-write becomes indivisible to other threads that use the same lock.
  • Use with lock: so the lock is released even on exceptions; keep critical sections small; every access to the shared data must use the same lock.
  • Races are timing-dependent and can hide: a tiny += 1 loop may lose nothing on a given CPython version, yet it is still wrong, and the free-threaded build removes the GIL entirely.
  • Alternatives: avoid sharing (queues, message passing, per-thread results), queue.Queue, immutable data; asyncio has races too, at await points.

Code / implementation expected: Yes β€” a shared counter or bank balance that loses updates without a lock and is correct with one.

  • threading
  • race-condition
  • locks

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 12 sections

Run the code

Edit the code and run it here. Each console line with an expected value becomes a test.

PythonWhy counter += 1 is not one step, and a lost update simulated step by step
PythonA real race with threads, forced with a Barrier, fixed with a Lock (OS threads, not in the browser)
# A race with real threads, made deterministic with a Barrier, then fixed.
# Not runnable in the browser: needs OS threads.
import threading

N = 4

# 1. Lost update: every thread reads before any thread writes.
balance = 0
barrier = threading.Barrier(N)

def deposit_unsafe():
    global balance
    seen = balance          # read
    barrier.wait()          # force the worst interleaving: all have read
    balance = seen + 100    # write, based on a stale value

threads = [threading.Thread(target=deposit_unsafe) for _ in range(N)]
for t in threads: t.start()
for t in threads: t.join()
print("unsafe: expected", N * 100, "got", balance)

# 2. Same code, but read-modify-write is inside one lock.
balance = 0
lock = threading.Lock()

def deposit_safe():
    global balance
    with lock:              # acquire ... release, even if an exception happens
        seen = balance
        balance = seen + 100

threads = [threading.Thread(target=deposit_safe) for _ in range(N)]
for t in threads: t.start()
for t in threads: t.join()
print("with lock: expected", N * 100, "got", balance)

# 3. Check-then-act race: two threads both see "not cached" and both compute.
cache, computed = {}, []
gate = threading.Barrier(2)

def get_unsafe(key):
    if key not in cache:     # check
        gate.wait()          # both threads passed the check
        computed.append(key)
        cache[key] = key.upper()   # act
    return cache[key]

ts = [threading.Thread(target=get_unsafe, args=("k",)) for _ in range(2)]
for t in ts: t.start()
for t in ts: t.join()
print("check-then-act without lock: computed", len(computed), "times")

cache, computed = {}, []
cache_lock = threading.Lock()

def get_safe(key):
    with cache_lock:
        if key not in cache:
            computed.append(key)
            cache[key] = key.upper()
        return cache[key]

ts = [threading.Thread(target=get_safe, args=("k",)) for _ in range(2)]
for t in ts: t.start()
for t in ts: t.join()
print("check-then-act with lock: computed", len(computed), "times")

# 4. What the lock object itself guarantees
print("locked inside with:", end=" ")
with lock:
    print(lock.locked())
print("locked after with:", lock.locked())
try:
    lock.release()
except RuntimeError as e:
    print("release an unlocked Lock:", e)

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 36 of 139 in Python

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Python questions
Up nextMedium

What are mixins in Python?

Continue