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 join as 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 multiprocessing change the sharing model, and what happens to the GIL bottleneck?

  • How would you extend the counter with decrement and a thread-safe compare_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

Check it out