Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Include/internal/pycore_ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,10 @@ _PyEval_SpecialMethodCanSuggest(PyObject *self, int oparg);
#define _PY_EVAL_PLEASE_STOP_BIT (1U << 5)
#define _PY_EVAL_EXPLICIT_MERGE_BIT (1U << 6)
#define _PY_EVAL_JIT_INVALIDATE_COLD_BIT (1U << 7)
#define _PY_EVAL_PROCESS_DELAYED_BIT (1U << 8)

/* Reserve a few bits for future use */
#define _PY_EVAL_EVENTS_BITS 8
#define _PY_EVAL_EVENTS_BITS 9
#define _PY_EVAL_EVENTS_MASK ((1 << _PY_EVAL_EVENTS_BITS)-1)

static inline void
Expand Down
72 changes: 72 additions & 0 deletions Lib/test/test_free_threading/test_qsbr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import threading
import unittest

from test.support import threading_helper


class Holder:
pass


def make_shared(func, *args):
t = threading.Thread(target=func, args=args)
t.start()
t.join()


@threading_helper.requires_working_threading()
class TestDelayedFreeReentrancy(unittest.TestCase):
ROUNDS = 1000

def queue_finalizers(self, callback):
class Finalizer:
def __del__(self):
callback()

holders = [Holder() for _ in range(self.ROUNDS)]
for h in holders:
h.__dict__ = {"f": Finalizer()}
return holders

def test_dict_resize(self):
d = {}
make_shared(d.get, 0)
holders = self.queue_finalizers(lambda: d.__setitem__(object(), 0))
for n, h in enumerate(holders):
h.__dict__ = {}
if n % 2:
h.__dict__ = {}
for k in list(d):
del d[k]
for i in range(6):
d[i] = i
self.assertEqual(len(d), len(list(d)))

def test_list_resize(self):
L = []
make_shared(L.append, 0)
holders = self.queue_finalizers(L.clear)
for n, h in enumerate(holders):
h.__dict__ = {}
if n % 2:
h.__dict__ = {}
L.clear()
for i in range(9):
L.insert(len(L), i)
self.assertEqual(len(L), len(list(L)))

def test_type_cache_invalidation(self):
class C:
x = None

holders = self.queue_finalizers(lambda: C.x)
for h in holders:
h.__dict__ = {}
C.x
new = object()
C.x = new
self.assertIs(C.x, new)
Comment on lines +58 to +68

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type cache is unfortunately affected too.



if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix crashes and hangs in the :term:`free-threaded build` when a destructor
ran in the middle of a :class:`dict` resize, a :class:`list` resize or a type
cache update. The thread's queue of delayed frees is now processed only from
the eval breaker, instead of synchronously when the queue fills up.
8 changes: 4 additions & 4 deletions Objects/obmalloc.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* Python's malloc wrappers (see pymem.h) */

#include "Python.h"
#include "pycore_ceval.h"
#include "pycore_interp.h" // _PyInterpreterState_HasFeature
#include "pycore_mmap.h" // _PyAnnotateMemoryMap()
#include "pycore_object.h" // _PyDebugAllocatorStats() definition
Expand Down Expand Up @@ -1513,10 +1514,8 @@ free_delayed(uintptr_t ptr, size_t size)
buf->wr_idx++;

if (buf->wr_idx == WORK_ITEMS_PER_CHUNK) {
// Normally the processing of delayed items is done from the eval
// breaker. Processing here is a safety measure to ensure too much
// work does not accumulate.
_PyMem_ProcessDelayed((PyThreadState *)tstate);
_Py_set_eval_breaker_bit((PyThreadState *)tstate,
_PY_EVAL_PROCESS_DELAYED_BIT);
}
#endif
}
Expand Down Expand Up @@ -1626,6 +1625,7 @@ _PyMem_ProcessDelayed(PyThreadState *tstate)
_PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate;

tstate_impl->qsbr->should_process = false;
_Py_unset_eval_breaker_bit(tstate, _PY_EVAL_PROCESS_DELAYED_BIT);

// Process thread-local work
process_queue(&tstate_impl->mem_free_queue, tstate_impl, true, NULL, NULL);
Expand Down
4 changes: 3 additions & 1 deletion Python/ceval_gil.c
Original file line number Diff line number Diff line change
Expand Up @@ -1389,7 +1389,9 @@ _Py_HandlePending(PyThreadState *tstate)
_Py_brc_merge_refcounts(tstate);
}
/* Process deferred memory frees held by QSBR */
if (_Py_qsbr_should_process(((_PyThreadStateImpl *)tstate)->qsbr)) {
if ((breaker & _PY_EVAL_PROCESS_DELAYED_BIT) != 0 ||
_Py_qsbr_should_process(((_PyThreadStateImpl *)tstate)->qsbr))
{
_PyMem_ProcessDelayed(tstate);
}
#endif
Expand Down
Loading