We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Concurrency Without Regret step 3 of 38
BoundedCounter: check-then-act under 64 threads
Build the counter that everyone gets wrong.
BoundedCounter(limit) must expose:
def try_increment(self) -> bool # increment and return True only if the result stays <= limit
def value(self) -> int # the current count
def snapshot(self) -> Mapping[str, int] # read-only view: {"value": ..., "granted": ...}
granted counts how many try_increment() calls returned True. If the
counter is correct, value == granted == min(limit, threads * calls) and the
sum of the per-thread win counts agrees with both.
Why it fails
The obvious implementation has two independent bugs in three lines:
def try_increment(self) -> bool:
if self._count < self._limit: # check
self._count += 1 # ...then act
return True
return False
Check-then-act. The read and the write are separate. Two threads sitting at
limit - 1 both see room and both take it, so the final count exceeds the
limit. A rate limiter that lets traffic through, a connection pool that opens
one connection too many, a licence check that grants a seat it does not have.
Read-modify-write. self._count += 1 is a load, an add and a store. Two
threads can both load the same value and both store the same successor, and one
increment disappears. So the count can also end up lower than the number of
True returns.
Under the first test case — 64 threads, 10,000 calls each, limit 5,000 — the naive version reliably fails both checks at once, in opposite directions.
What to write
Only BoundedCounter. The driver that spawns the threads and assembles the
report is provided.
Take the lock around the whole decision, not just the mutation. Locking
only self._count += 1 fixes the lost update and leaves the over-grant, which
is the more damaging of the two.
Where the type system earns its keep
Two annotations are doing real work here:
-
Finalon the lock and the limit. A lock that gets rebound is not a lock; a limit that another thread can change is not a limit.Finalturns “assigned once in__init__“ from a convention into something mypy checks. -
Mapping[str, int]onsnapshot. The return type says callers may not mutate. Butdictis aMapping, so the annotation alone still hands out a live, mutable, unsynchronised handle on your internal state to anyone who does not type-check. Build the snapshot inside the lock and wrap it intypes.MappingProxyType— the report’sread_onlyfield checks that the returned object genuinely has no__setitem__.
Loading visualization…
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.