L2 · Working engineerAdvanced L2~8 min · 2 tests#83

Thread-Safe Counter With a Lock

Make a counter safe across threads with threading.Lock so 10 threads × 1000 increments always give 10000. Learn race conditions and the GIL myth.

The problem

Write a Counter class with increment() and a value attribute that is safe to call from many threads at once.

The test starts 10 threads, each calling increment() 1000 times, and expects 10000. It also checks that the counter guards its update with a threading.Lock (or RLock).

Running in your browser: Python here runs on WebAssembly, which has no OS threads or processes. The standard APIs still work — threading, concurrent.futures, multiprocessing, asyncio — but they run on a deterministic simulator: threads run to completion when started, pools run tasks in order, and asyncio uses a virtual clock (await asyncio.sleep(0.2) advances time by 0.2 s instantly). Write exactly the code you would write in the interview.

Examples

  1. Example 1

    Input

    hammer(Counter)

    Expected output

    10000
  2. Example 2

    Input

    uses_lock(Counter)

    Expected output

    True

Edge cases to ask about

  • Lock created per instance
  • Exception inside the critical section
How the tests call your code

These helpers run before your code. The test inputs above call them.

import threading

def hammer(Counter):
    c = Counter()
    def work():
        for _ in range(1000):
            c.increment()
    threads = [threading.Thread(target=work) for _ in range(10)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    return c.value

def uses_lock(Counter):
    lock_types = (type(threading.Lock()), type(threading.RLock()))
    c = Counter()
    found = [v for v in list(vars(c).values()) + list(vars(type(c)).values()) if isinstance(v, lock_types)]
    return len(found) > 0

Hints

0/3

    How an interviewer scores this

    0/9
    Python 3.13 · Counter
    ⌘/Ctrl + Enter runs the examples

    Your code runs in real CPython inside your browser — nothing is sent anywhere. The first run downloads the interpreter (about 6 MB, once). Your code is saved on this device as you type.

    Complexity Lab

    What does this cost as n grows?

    Interviewers score the analysis as much as the code. Commit to an answer first — then check it, and read why.

    Time complexity of the optimal solution
    Space complexity (extra memory)

    Pick both to reveal the answer.

    From brute force to optimal

    The progression an interviewer wants to hear, one step at a time.

    ApproachTimeSpaceIdea
    Unprotected +=O(1)O(1)Two threads can read the same old value — an increment is lost.
    bestthreading.Lock around the updateO(1)O(1)Only one thread runs the critical section at a time.
    Walkthrough of the optimal approach (try it yourself first)

    self.value += 1 is a read-modify-write: two threads can both read 41 and both write 42, losing an increment. A threading.Lock makes the critical section run one thread at a time; with self._lock: guarantees release even if an exception occurs.

    The GIL does not make this safe. It stops two threads executing bytecode at the same instant, but a thread can be switched out between the read and the write. (Free-threaded Python 3.13+ removes the GIL entirely.) Real CPython loses counts without the lock under load.

    Complexity: O(1) time, O(1) space. Each increment is constant work; the lock adds a constant overhead (and contention under load).

    Reveal the reference solution
    import threading
    
    class Counter:
        def __init__(self):
            self.value = 0
            self._lock = threading.Lock()
    
        def increment(self):
            with self._lock:
                self.value += 1

    Follow-ups interviewers ask

    • Is itertools.count() thread-safe?
    • Lock vs RLock vs Semaphore.

    Frequently asked interview questions

    Core interview concepts, complexities, and follow-ups scored by hiring teams.

    What is the time complexity of Thread-Safe Counter With a Lock in Python?

    The optimal solution runs in O(1) time and O(1) auxiliary space. Each increment is constant work; the lock adds a constant overhead (and contention under load).

    What is the brute-force approach, and how do you optimise it?

    Unprotected +=: O(1) time, O(1) space. Two threads can read the same old value — an increment is lost. threading.Lock around the update: O(1) time, O(1) space. Only one thread runs the critical section at a time.

    What follow-up questions do interviewers ask about Thread-Safe Counter With a Lock?

    Is itertools.count() thread-safe? Lock vs RLock vs Semaphore.