We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Parallel Python with Process Pools step 24 of 24
Build a Thread-Safe Cache with One Lock per Key
Your web app caches expensive results, such as one rendered report per customer. On a miss, it computes the report and stores it. Two threads can both see the miss for one customer and both do the expensive work. The GIL does not stop that, because a thread can pause between the check and the store. On the free-threaded build from the previous article, the two threads can even run those steps at the same instant.
One lock around the whole cache fixes the race, but then every customer waits for the slowest report. A lock per key is better. Callers for the same key wait for the first one, and callers for different keys compute at the same time.
cache: MemoCache[str, str] = MemoCache()
cache.get_or_compute("alpha", lambda: "ALPHA") # calls the lambda: 'ALPHA'
cache.get_or_compute("alpha", lambda: "other") # cached: 'ALPHA'
cache.view # mappingproxy({'alpha': 'ALPHA'})
Write the cache
Complete MemoCache[K, V]. Its method
get_or_compute(key, compute: Callable[[], V]) -> V follows these rules:
- For any one key, at most one callback runs at a time. Once a callback succeeds, store its value. Return it to every later caller for that key without calling their callbacks.
- Never run a callback while you hold a lock that callers for other keys
need. A shared bookkeeping lock that you hold only briefly is fine.
Release it before you call
computeor wait for a key's lock. - If a callback raises, the exception reaches that caller and the key stays uncached. Release the key's lock so another caller can try again. More than one callback can therefore run for one key in total, when some of them fail.
- The cache stores any value, including
None,Falseand0. Test whether the key is in the dictionary, not whether the value is truthy.
If callers pass different callbacks for one key, the first to succeed decides the value. Add no eviction or expiry.
The view property returns a read-only Mapping[K, V] snapshot. Copy the
dictionary while you hold the bookkeeping lock, and wrap the copy in
types.MappingProxyType. A proxy over the live dictionary would show later
inserts. The values are not copied, so a cached list is still the same list.
Keep each key's lock once you create it. Deleting it while a thread waits on
it would give two threads different locks for one key. You can assume that
keys hash consistently. Per-key locks cannot prevent a deadlock where a
callback asks the cache for its own key, or for a key whose callback is
waiting on it.
You can assume no callback does that. Make the code pass mypy --strict.
Write the driver
solve(*, key, threads, distinct_keys, flaky_key) runs three checks, each
on a new cache, and returns
(hot_calls, hot_value, overlap_flag, flaky_calls, flaky_value).
- Same key. Start
threadsthreads that each callget_or_computeforkey. The callback counts its calls under a lock and returnskey.upper(). Wait for every thread, and let unexpected exceptions propagate. Then callget_or_computeforkeyonce more. Withthreads=0, that last call is the one that computes.hot_callsmust be 1. - Different keys. Make
distinct_keyskeys namedk0,k1and so on, and run one thread per key, all at once. Each callback waits on athreading.Barrier(distinct_keys)with a two-second timeout, then returns its key. If one lock covered every callback, they could never all reach the barrier.overlap_flagisTrueif every thread gets through andFalseif the barrier breaks. Let other exceptions propagate. With one key, the flag isTrue. - Retry. For
flaky_key, the callback raisesRuntimeErroron its first call and returnsflaky_key.upper()on its second. Catch the first failure and call again. Then call a third time to check that the value stayed cached.flaky_callsmust be 2.
Raise ValueError if threads is negative or distinct_keys is below 1.
The tests start at most 64 threads in each check.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.