@@ -8015,13 +8015,19 @@ validate_watcher_id(PyInterpreterState *interp, int watcher_id)
80158015 PyErr_Format (PyExc_ValueError , "Invalid dict watcher ID %d" , watcher_id );
80168016 return -1 ;
80178017 }
8018- if (!interp -> dict_state .watchers [watcher_id ]) {
8018+ PyDict_WatchCallback cb = FT_ATOMIC_LOAD_PTR_RELAXED (
8019+ interp -> dict_state .watchers [watcher_id ]);
8020+ if (cb == NULL ) {
80198021 PyErr_Format (PyExc_ValueError , "No dict watcher set for ID %d" , watcher_id );
80208022 return -1 ;
80218023 }
80228024 return 0 ;
80238025}
80248026
8027+ // In free-threaded builds, Add/Clear serialize on watcher_mutex and publish
8028+ // callbacks with release stores. SendEvent reads them lock-free using
8029+ // acquire loads.
8030+
80258031int
80268032PyDict_Watch (int watcher_id , PyObject * dict )
80278033{
@@ -8033,7 +8039,8 @@ PyDict_Watch(int watcher_id, PyObject* dict)
80338039 if (validate_watcher_id (interp , watcher_id )) {
80348040 return -1 ;
80358041 }
8036- FT_ATOMIC_OR_UINT64 (((PyDictObject * )dict )-> _ma_watcher_tag , (1LL << watcher_id ));
8042+ FT_ATOMIC_OR_UINT64 (((PyDictObject * )dict )-> _ma_watcher_tag ,
8043+ 1ULL << watcher_id );
80378044 return 0 ;
80388045}
80398046
@@ -8048,36 +8055,48 @@ PyDict_Unwatch(int watcher_id, PyObject* dict)
80488055 if (validate_watcher_id (interp , watcher_id )) {
80498056 return -1 ;
80508057 }
8051- FT_ATOMIC_AND_UINT64 (((PyDictObject * )dict )-> _ma_watcher_tag , ~(1LL << watcher_id ));
8058+ FT_ATOMIC_AND_UINT64 (((PyDictObject * )dict )-> _ma_watcher_tag ,
8059+ ~(1ULL << watcher_id ));
80528060 return 0 ;
80538061}
80548062
80558063int
80568064PyDict_AddWatcher (PyDict_WatchCallback callback )
80578065{
8066+ int watcher_id = -1 ;
80588067 PyInterpreterState * interp = _PyInterpreterState_GET ();
80598068
8069+ FT_MUTEX_LOCK_FLAGS (& interp -> dict_state .watcher_mutex ,
8070+ _Py_LOCK_DONT_DETACH );
80608071 /* Some watchers are reserved for CPython, start at the first available one */
80618072 for (int i = FIRST_AVAILABLE_WATCHER ; i < DICT_MAX_WATCHERS ; i ++ ) {
80628073 if (!interp -> dict_state .watchers [i ]) {
8063- interp -> dict_state .watchers [i ] = callback ;
8064- return i ;
8074+ FT_ATOMIC_STORE_PTR_RELEASE (interp -> dict_state .watchers [i ], callback );
8075+ watcher_id = i ;
8076+ goto done ;
80658077 }
80668078 }
8067-
80688079 PyErr_SetString (PyExc_RuntimeError , "no more dict watcher IDs available" );
8069- return -1 ;
8080+ done :
8081+ FT_MUTEX_UNLOCK (& interp -> dict_state .watcher_mutex );
8082+ return watcher_id ;
80708083}
80718084
80728085int
80738086PyDict_ClearWatcher (int watcher_id )
80748087{
8088+ int res = 0 ;
80758089 PyInterpreterState * interp = _PyInterpreterState_GET ();
8090+ FT_MUTEX_LOCK_FLAGS (& interp -> dict_state .watcher_mutex ,
8091+ _Py_LOCK_DONT_DETACH );
80768092 if (validate_watcher_id (interp , watcher_id )) {
8077- return -1 ;
8093+ res = -1 ;
8094+ goto done ;
80788095 }
8079- interp -> dict_state .watchers [watcher_id ] = NULL ;
8080- return 0 ;
8096+ FT_ATOMIC_STORE_PTR_RELEASE (interp -> dict_state .watchers [watcher_id ], NULL );
8097+ done :
8098+ FT_MUTEX_UNLOCK (& interp -> dict_state .watcher_mutex );
8099+ return res ;
80818100}
80828101
80838102static const char *
@@ -8102,7 +8121,8 @@ _PyDict_SendEvent(int watcher_bits,
81028121 PyInterpreterState * interp = _PyInterpreterState_GET ();
81038122 for (int i = 0 ; i < DICT_MAX_WATCHERS ; i ++ ) {
81048123 if (watcher_bits & 1 ) {
8105- PyDict_WatchCallback cb = interp -> dict_state .watchers [i ];
8124+ PyDict_WatchCallback cb = FT_ATOMIC_LOAD_PTR_ACQUIRE (
8125+ interp -> dict_state .watchers [i ]);
81068126 if (cb && (cb (event , (PyObject * )mp , key , value ) < 0 )) {
81078127 // We don't want to resurrect the dict by potentially having an
81088128 // unraisablehook keep a reference to it, so we don't pass the
0 commit comments