gh-145235: Make dict watcher API thread-safe for free-threaded builds (gh-145233)

In free-threaded builds, concurrent calls to PyDict_AddWatcher, PyDict_ClearWatcher, PyDict_Watch, and PyDict_Unwatch can race on the shared callback array and the per-dict watcher tags. This change adds a mutex to serialize watcher registration and removal, atomic operations for tag updates, and atomic acquire/release synchronization for callback dispatch in _PyDict_SendEvent.
This commit is contained in:
Alper 2026-05-11 08:39:55 -07:00 committed by GitHub
parent fadd9bc14e
commit 8a4895985f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 159 additions and 27 deletions

View file

@ -18,6 +18,7 @@
#include "pycore_opcode_metadata.h"
#include "pycore_opcode_utils.h"
#include "pycore_pystate.h" // _PyInterpreterState_GET()
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_*
#include "pycore_tstate.h" // _PyThreadStateImpl
#include "pycore_uop_metadata.h"
#include "pycore_long.h"
@ -127,7 +128,7 @@ static void
increment_mutations(PyObject* dict) {
assert(PyDict_CheckExact(dict));
PyDictObject *d = (PyDictObject *)dict;
FT_ATOMIC_ADD_UINT64(d->_ma_watcher_tag, (1 << DICT_MAX_WATCHERS));
FT_ATOMIC_ADD_UINT64(d->_ma_watcher_tag, 1ULL << DICT_MAX_WATCHERS);
}
/* The first two dict watcher IDs are reserved for CPython,
@ -156,6 +157,17 @@ type_watcher_callback(PyTypeObject* type)
return 0;
}
static int
_setup_optimizer_watchers(void *Py_UNUSED(arg))
{
PyInterpreterState *interp = _PyInterpreterState_GET();
FT_ATOMIC_STORE_PTR_RELEASE(
interp->dict_state.watchers[GLOBALS_WATCHER_ID],
globals_watcher_callback);
interp->type_watchers[TYPE_WATCHER_ID] = type_watcher_callback;
return 0;
}
static void
watch_type(PyTypeObject *type, _PyBloomFilter *filter)
{
@ -580,10 +592,8 @@ optimize_uops(
// Make sure that watchers are set up
PyInterpreterState *interp = _PyInterpreterState_GET();
if (interp->dict_state.watchers[GLOBALS_WATCHER_ID] == NULL) {
interp->dict_state.watchers[GLOBALS_WATCHER_ID] = globals_watcher_callback;
interp->type_watchers[TYPE_WATCHER_ID] = type_watcher_callback;
}
_PyOnceFlag_CallOnce(&interp->dict_state.watcher_setup_once,
_setup_optimizer_watchers, NULL);
_Py_uop_abstractcontext_init(ctx, dependencies);
_Py_UOpsAbstractFrame *frame = _Py_uop_frame_new(ctx, (PyCodeObject *)func->func_code, NULL, 0);

View file

@ -320,6 +320,7 @@ _Py_COMP_DIAG_POP
&(runtime)->allocators.mutex, \
&(runtime)->_main_interpreter.types.mutex, \
&(runtime)->_main_interpreter.code_state.mutex, \
&(runtime)->_main_interpreter.dict_state.watcher_mutex, \
}
static void