Shared Counter
Overview
Implement an integer counter that starts at zero. Several worker threads run concurrently and each adds to it, until the counter has been incremented a fixed total number of times.
The core problem is safe access to shared mutable state. The increment counter += 1 is not atomic — it compiles to a read, an add, and a write. If two threads interleave on those steps, both read the same value, both add one, and both write back the same result. One update is lost. This is a classic race condition, and the missing increment is the lost-update problem.
The fix is mutual exclusion: make the read-modify-write a critical section guarded by a lock, so only one thread is inside it at a time. The work to design here is the thread lifecycle (spawn, run, join), the work split across threads, and the synchronization that keeps the final value correct.
Interfaces
A ThreadSafeCounter, plus a function that spawns and drives the workers:
import threading
class ThreadSafeCounter:
def __init__(self, start=0):
self._value = start
self._lock = threading.Lock() # mutex guarding _value
def increment(self, by=1):
"""Atomically add to the counter; safe under concurrent callers."""
with self._lock: # enter critical section
self._value += by
@property
def value(self):
with self._lock:
return self._value
def run_concurrent_increments(num_threads, total_increments):
"""
Spawn `num_threads` worker threads that together apply `total_increments`
increments to one shared counter, then join them all. Return the final
value, which must equal total_increments.
"""
...
The with self._lock block acquires the lock on entry and releases it on exit, so the read-modify-write is serialized. The value getter also locks, so a reader never observes a half-updated state and gets a proper happens-before guarantee against the writers.
Inputs and outputs
- Input: the thread count (1–1024) and the total increment count (0–10 million).
- Output: the final counter value. When the synchronization is correct, it equals the requested total on every run, independent of thread scheduling or interleaving.
Requirements
- Partition the total increments across the threads as evenly as possible.
- Guard every read-modify-write with the lock so no update is lost.
- Use
joinas a barrier to wait for all workers before reading the result. - Standard library only; rely on blocking locks, not busy-waiting / spin loops.
Examples
run_concurrent_increments(4, 1000) # 1000
run_concurrent_increments(1, 0) # 0
run_concurrent_increments(8, 1_000_000) # 1000000
Cover the edge cases: zero increments (no work to do), more threads than increments (some threads get an empty share), and a total that does not divide evenly (the remainder must still be applied exactly once).
Think about it
You drop the lock because CPython has a GIL anyway — surely counter += 1 is safe now?
The interpreter can still switch threads between the read, the add, and the write.
So what does the GIL actually guarantee here, and what does it not?
Follow-ups
How would you replace the lock with an atomic compare-and-swap (CAS) loop — and would it actually be faster under heavy contention?
How does moving to
multiprocessingchange the sharing model, and what happens to the GIL bottleneck?How would you extend the counter with
decrementand a thread-safecompare_and_set?And more...
FANG Crash Course
Fullset Newest FANG Coding/System Design Questions, Optimal Solutions and Explanations.
Step_1 Matched with a FAANG+ Senior Engineer
Step_2 Fullset FANG Coding/System Design Questions
Step_3 Solutions + step by step Explanation
