You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
a == b and a.update(b) deadlock when another thread does the same with the arguments swapped #118
a == b on one thread and b == a on another can deadlock, and so can a.update(b) with b.update(a). It happens on both the GIL and the free-threaded builds (6.2.9, Python 3.13 and 3.14t, 3 of 3 runs each):
With one thread it finishes in under 0.1 s; with two it never prints ok. On 3.14t it also hangs with plain int keys. On the GIL build it needs keys whose __hash__/__eq__ are Python code, so the GIL can switch while a lock is held.
The two locks are taken in argument order:
__eq__ / __ne__ lock self, then other (e.g. src/pyclasses/lrucache.rs, policy_py_attached twice).
update(other_cache) holds self's lock for the whole Wrapped::extend, and iterating other.items() takes other's lock on every step.
So thread 1 holds a and waits for b while thread 2 holds b and waits for a.
Possible fixes: lock the two caches in a fixed order (e.g. by address) in __eq__/__ne__, and in update either copy the other cache's items before taking self's lock or lock both in the same fixed order. Which would you prefer? I can send a PR.
Thanks! The fixed order for __eq__/__ne__ is in #122.
For update a fixed order doesn't work: a.update(b) keeps a's lock while it iterates b.items(), and every step of that iterator takes b's lock, so locking both up front would block on b a second time. The root cause there is that update runs the caller's iteration (and __hash__, getsizeof) while holding the lock, which also hangs on a single thread:
insert already builds the handle (hash, getsizeof) before taking the lock. update could do the same for its whole input: collect the handles first, then take the lock once and insert them. That fixes both the two-thread case and this one, at the cost of a temporary list of the new entries. Would that be OK as a separate PR?
That fixes both the two-thread case and this one, at the cost of a temporary list of the new entries. Would that be OK as a separate PR?
Yes I'd appreciate that. You're right. Of course, when the input is a list, set, or tuple, there's no need for a temporary list, but for the others, it is needed.
Activity
awolverp commentedon Oct 29, 2026
Thanks again
Locking two caches in a fixed order is the better solution
greenclaw commentedon Oct 29, 2026
Thanks! The fixed order for
__eq__/__ne__is in #122.For
updatea fixed order doesn't work:a.update(b)keepsa's lock while it iteratesb.items(), and every step of that iterator takesb's lock, so locking both up front would block onba second time. The root cause there is thatupdateruns the caller's iteration (and__hash__,getsizeof) while holding the lock, which also hangs on a single thread:insertalready builds the handle (hash,getsizeof) before taking the lock.updatecould do the same for its whole input: collect the handles first, then take the lock once and insert them. That fixes both the two-thread case and this one, at the cost of a temporary list of the new entries. Would that be OK as a separate PR?fix: lock two caches in address order in __eq__ and __ne__ (#118)
awolverp commentedon Oct 29, 2026
Yes I'd appreciate that. You're right. Of course, when the input is a list, set, or tuple, there's no need for a temporary list, but for the others, it is needed.
greenclaw commentedon Oct 29, 2026
fix is open as separate pr
fix: build the handles for update() before taking the cache lock (#118)