Skip to content

Inserting from two threads deadlocks when evicted values have __del__ #95

Description

@nimbrel

Two threads inserting into a full LRUCache hang forever on the GIL build (3.12, 3.14) when the values have a __del__. No gc.collect(), sleep or reentrant call is involved, so this is not the collector case from #84 that #86 fixed. With one thread, or without __del__, it finishes in about 0.1 s. On 3.14t it finishes, but hangs once a third thread runs gc.collect() every 10 ms.

import threading

import cachebox

cache = cachebox.LRUCache(100)


class Value:
    def __del__(self):
        self.dropped = True  # any Python-level finalizer


def insert(base):
    for i in range(200_000):
        cache[base + i % 1000] = Value()  # the cache is full, so this evicts an older Value


threads = [threading.Thread(target=insert, args=(b,)) for b in (0, 10_000)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print("finished")

It never prints finished: killed by timeout 60 in 3/3 runs on each of 3.12.12 and 3.14.7. faulthandler shows one thread in Value.__del__ called from the insert line, and the other on the insert line.

cachebox 6.2.7 from PyPI (src/ unchanged on main at 9af1cec), CPython 3.12.12, 3.14.7 and 3.14.7t, Linux x86_64.

insert evicts while holding the cache's parking_lot mutex, and dropping the evicted value runs its __del__ under that lock. When __del__ gives up the GIL at the switch interval, the other thread takes the GIL and blocks in lock() without releasing it. On 3.14t the blocked thread stays attached, so a collection's stop-the-world pause waits for it forever.

pub fn insert(
&self,
py: pyo3::Python<'_>,
handle: P::Handle,
) -> pyo3::PyResult<Option<P::Handle>> {
let mut lock = self.inner.lock();
self.insert_no_lock(&mut lock, py, handle)
}

fn evict(&mut self) -> pyo3::PyResult<()> {
self.policy.evict(self.shared)?;
Ok(())
}

An untested idea: wait for the lock with PyO3's MutexExt::lock_py_attached (feature parking_lot), which detaches while blocked, or drop evicted values after the lock is released.

Found while testing with ftcheck.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugSomething isn't workingCritical

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions