@@ -7842,13 +7842,19 @@ validate_watcher_id(PyInterpreterState *interp, int watcher_id)
78427842 PyErr_Format (PyExc_ValueError , "Invalid dict watcher ID %d" , watcher_id );
78437843 return -1 ;
78447844 }
7845- if (!interp -> dict_state .watchers [watcher_id ]) {
7845+ PyDict_WatchCallback cb = FT_ATOMIC_LOAD_PTR_RELAXED (
7846+ interp -> dict_state .watchers [watcher_id ]);
7847+ if (cb == NULL ) {
78467848 PyErr_Format (PyExc_ValueError , "No dict watcher set for ID %d" , watcher_id );
78477849 return -1 ;
78487850 }
78497851 return 0 ;
78507852}
78517853
7854+ // In free-threaded builds, Add/Clear serialize on watcher_mutex and publish
7855+ // callbacks with release stores. SendEvent reads them lock-free using
7856+ // acquire loads.
7857+
78527858int
78537859PyDict_Watch (int watcher_id , PyObject * dict )
78547860{
@@ -7860,7 +7866,9 @@ PyDict_Watch(int watcher_id, PyObject* dict)
78607866 if (validate_watcher_id (interp , watcher_id )) {
78617867 return -1 ;
78627868 }
7863- ((PyDictObject * )dict )-> _ma_watcher_tag |= (1LL << watcher_id );
7869+ Py_BEGIN_CRITICAL_SECTION (dict );
7870+ ((PyDictObject * )dict )-> _ma_watcher_tag |= (1ULL << watcher_id );
7871+ Py_END_CRITICAL_SECTION ();
78647872 return 0 ;
78657873}
78667874
@@ -7875,36 +7883,47 @@ PyDict_Unwatch(int watcher_id, PyObject* dict)
78757883 if (validate_watcher_id (interp , watcher_id )) {
78767884 return -1 ;
78777885 }
7878- ((PyDictObject * )dict )-> _ma_watcher_tag &= ~(1LL << watcher_id );
7886+ Py_BEGIN_CRITICAL_SECTION (dict );
7887+ ((PyDictObject * )dict )-> _ma_watcher_tag &= ~(1ULL << watcher_id );
7888+ Py_END_CRITICAL_SECTION ();
78797889 return 0 ;
78807890}
78817891
78827892int
78837893PyDict_AddWatcher (PyDict_WatchCallback callback )
78847894{
7895+ int watcher_id = -1 ;
78857896 PyInterpreterState * interp = _PyInterpreterState_GET ();
78867897
7898+ FT_MUTEX_LOCK (& interp -> dict_state .watcher_mutex );
78877899 /* Some watchers are reserved for CPython, start at the first available one */
78887900 for (int i = FIRST_AVAILABLE_WATCHER ; i < DICT_MAX_WATCHERS ; i ++ ) {
78897901 if (!interp -> dict_state .watchers [i ]) {
7890- interp -> dict_state .watchers [i ] = callback ;
7891- return i ;
7902+ FT_ATOMIC_STORE_PTR_RELEASE (interp -> dict_state .watchers [i ], callback );
7903+ watcher_id = i ;
7904+ goto done ;
78927905 }
78937906 }
7894-
78957907 PyErr_SetString (PyExc_RuntimeError , "no more dict watcher IDs available" );
7896- return -1 ;
7908+ done :
7909+ FT_MUTEX_UNLOCK (& interp -> dict_state .watcher_mutex );
7910+ return watcher_id ;
78977911}
78987912
78997913int
79007914PyDict_ClearWatcher (int watcher_id )
79017915{
7916+ int res = 0 ;
79027917 PyInterpreterState * interp = _PyInterpreterState_GET ();
7918+ FT_MUTEX_LOCK (& interp -> dict_state .watcher_mutex );
79037919 if (validate_watcher_id (interp , watcher_id )) {
7904- return -1 ;
7920+ res = -1 ;
7921+ goto done ;
79057922 }
7906- interp -> dict_state .watchers [watcher_id ] = NULL ;
7907- return 0 ;
7923+ FT_ATOMIC_STORE_PTR_RELEASE (interp -> dict_state .watchers [watcher_id ], NULL );
7924+ done :
7925+ FT_MUTEX_UNLOCK (& interp -> dict_state .watcher_mutex );
7926+ return res ;
79087927}
79097928
79107929static const char *
@@ -7929,7 +7948,8 @@ _PyDict_SendEvent(int watcher_bits,
79297948 PyInterpreterState * interp = _PyInterpreterState_GET ();
79307949 for (int i = 0 ; i < DICT_MAX_WATCHERS ; i ++ ) {
79317950 if (watcher_bits & 1 ) {
7932- PyDict_WatchCallback cb = interp -> dict_state .watchers [i ];
7951+ PyDict_WatchCallback cb = FT_ATOMIC_LOAD_PTR_ACQUIRE (
7952+ interp -> dict_state .watchers [i ]);
79337953 if (cb && (cb (event , (PyObject * )mp , key , value ) < 0 )) {
79347954 // We don't want to resurrect the dict by potentially having an
79357955 // unraisablehook keep a reference to it, so we don't pass the
0 commit comments