|
20 | 20 | from collections import namedtuple |
21 | 21 | from functools import wraps, partial as functools_partial |
22 | 22 | from inspect import signature |
| 23 | +from threading import RLock |
23 | 24 | from typing import get_type_hints |
24 | 25 |
|
25 | 26 | from .arity import (_resolve_bindings, tuplify_bindings, _bind) |
@@ -61,18 +62,35 @@ def memoize(f): |
61 | 62 |
|
62 | 63 | **CAUTION**: ``f`` must be pure (no side effects, no internal state |
63 | 64 | preserved between invocations) for this to make any sense. |
| 65 | +
|
| 66 | + Beginning with v0.15.0, `memoize` is thread-safe even when the same memoized |
| 67 | + function instance is called concurrently from multiple threads. Exactly one |
| 68 | + thread will compute the result. If `f` is recursive, the thread that acquired |
| 69 | + the lock is the one that is allowed to recurse into the memoized `f`. |
64 | 70 | """ |
| 71 | + # One lock per use site of `memoize`. We use an `RLock` to allow recursive calls |
| 72 | + # to the memoized `f` in the thread that acquired the lock. |
| 73 | + lock = RLock() |
65 | 74 | memo = {} |
66 | 75 | @wraps(f) |
67 | 76 | def memoized(*args, **kwargs): |
68 | 77 | k = tuplify_bindings(_resolve_bindings(f, args, kwargs, _partial=False)) |
69 | | - if k not in memo: |
70 | | - try: |
71 | | - result = (_success, maybe_force_args(f, *args, **kwargs)) |
72 | | - except BaseException as err: |
73 | | - result = (_fail, err) |
74 | | - memo[k] = result # should yell separately if k is not a valid key |
75 | | - kind, value = memo[k] |
| 78 | + try: # EAFP to eliminate TOCTTOU. |
| 79 | + kind, value = memo[k] |
| 80 | + except KeyError: |
| 81 | + # But we still need to be careful to avoid race conditions. |
| 82 | + with lock: |
| 83 | + if k not in memo: |
| 84 | + # We were the first thread to acquire the lock. |
| 85 | + try: |
| 86 | + result = (_success, maybe_force_args(f, *args, **kwargs)) |
| 87 | + except BaseException as err: |
| 88 | + result = (_fail, err) |
| 89 | + memo[k] = result # should yell separately if k is not a valid key |
| 90 | + else: |
| 91 | + # Some other thread acquired the lock before us. |
| 92 | + pass |
| 93 | + kind, value = memo[k] |
76 | 94 | if kind is _fail: |
77 | 95 | raise value |
78 | 96 | return value |
|
0 commit comments