diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst index d63c26865899cc..6f79a91642c9d1 100644 --- a/Doc/c-api/dict.rst +++ b/Doc/c-api/dict.rst @@ -89,6 +89,15 @@ Dictionary objects ``0`` on success or ``-1`` on failure. This function *does not* steal a reference to *val*. + .. note:: + + In the :term:`free-threaded build`, key hashing via + :meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__` + can execute arbitrary Python code, during which the :term:`per-object + lock` may be temporarily released. For built-in key types + (:class:`str`, :class:`int`, :class:`float`), the lock is not released + during comparison. + .. c:function:: int PyDict_SetItemString(PyObject *p, const char *key, PyObject *val) @@ -96,6 +105,15 @@ Dictionary objects specified as a :c:expr:`const char*` UTF-8 encoded bytes string, rather than a :c:expr:`PyObject*`. + .. note:: + + In the :term:`free-threaded build`, key hashing via + :meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__` + can execute arbitrary Python code, during which the :term:`per-object + lock` may be temporarily released. For built-in key types + (:class:`str`, :class:`int`, :class:`float`), the lock is not released + during comparison. + .. c:function:: int PyDict_DelItem(PyObject *p, PyObject *key) @@ -104,6 +122,15 @@ Dictionary objects If *key* is not in the dictionary, :exc:`KeyError` is raised. Return ``0`` on success or ``-1`` on failure. + .. note:: + + In the :term:`free-threaded build`, key hashing via + :meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__` + can execute arbitrary Python code, during which the :term:`per-object + lock` may be temporarily released. For built-in key types + (:class:`str`, :class:`int`, :class:`float`), the lock is not released + during comparison. + .. c:function:: int PyDict_DelItemString(PyObject *p, const char *key) @@ -111,6 +138,15 @@ Dictionary objects specified as a :c:expr:`const char*` UTF-8 encoded bytes string, rather than a :c:expr:`PyObject*`. + .. note:: + + In the :term:`free-threaded build`, key hashing via + :meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__` + can execute arbitrary Python code, during which the :term:`per-object + lock` may be temporarily released. For built-in key types + (:class:`str`, :class:`int`, :class:`float`), the lock is not released + during comparison. + .. c:function:: int PyDict_GetItemRef(PyObject *p, PyObject *key, PyObject **result) @@ -139,6 +175,13 @@ Dictionary objects :meth:`~object.__eq__` methods are silently ignored. Prefer the :c:func:`PyDict_GetItemWithError` function instead. + .. note:: + + In the :term:`free-threaded build`, the returned + :term:`borrowed reference` may become invalid if another thread modifies + the dictionary concurrently. Prefer :c:func:`PyDict_GetItemRef`, which + returns a :term:`strong reference`. + .. versionchanged:: 3.10 Calling this API without an :term:`attached thread state` had been allowed for historical reason. It is no longer allowed. @@ -151,6 +194,13 @@ Dictionary objects occurred. Return ``NULL`` **without** an exception set if the key wasn't present. + .. note:: + + In the :term:`free-threaded build`, the returned + :term:`borrowed reference` may become invalid if another thread modifies + the dictionary concurrently. Prefer :c:func:`PyDict_GetItemRef`, which + returns a :term:`strong reference`. + .. c:function:: PyObject* PyDict_GetItemString(PyObject *p, const char *key) @@ -166,6 +216,13 @@ Dictionary objects Prefer using the :c:func:`PyDict_GetItemWithError` function with your own :c:func:`PyUnicode_FromString` *key* instead. + .. note:: + + In the :term:`free-threaded build`, the returned + :term:`borrowed reference` may become invalid if another thread modifies + the dictionary concurrently. Prefer :c:func:`PyDict_GetItemStringRef`, + which returns a :term:`strong reference`. + .. c:function:: int PyDict_GetItemStringRef(PyObject *p, const char *key, PyObject **result) @@ -186,6 +243,14 @@ Dictionary objects .. versionadded:: 3.4 + .. note:: + + In the :term:`free-threaded build`, the returned + :term:`borrowed reference` may become invalid if another thread modifies + the dictionary concurrently. Prefer :c:func:`PyDict_SetDefaultRef`, + which returns a :term:`strong reference`. + + .. c:function:: int PyDict_SetDefaultRef(PyObject *p, PyObject *key, PyObject *default_value, PyObject **result) @@ -224,6 +289,15 @@ Dictionary objects .. versionadded:: 3.13 + .. note:: + + In the :term:`free-threaded build`, key hashing via + :meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__` + can execute arbitrary Python code, during which the :term:`per-object + lock` may be temporarily released. For built-in key types + (:class:`str`, :class:`int`, :class:`float`), the lock is not released + during comparison. + .. c:function:: int PyDict_PopString(PyObject *p, const char *key, PyObject **result) @@ -233,6 +307,15 @@ Dictionary objects .. versionadded:: 3.13 + .. note:: + + In the :term:`free-threaded build`, key hashing via + :meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__` + can execute arbitrary Python code, during which the :term:`per-object + lock` may be temporarily released. For built-in key types + (:class:`str`, :class:`int`, :class:`float`), the lock is not released + during comparison. + .. c:function:: PyObject* PyDict_Items(PyObject *p) @@ -338,6 +421,13 @@ Dictionary objects only be added if there is not a matching key in *a*. Return ``0`` on success or ``-1`` if an exception was raised. + .. note:: + + In the :term:`free-threaded build`, when *b* is a + :class:`dict` (with the standard iterator), both *a* and *b* are locked + for the duration of the operation. When *b* is a non-dict mapping, only + *a* is locked; *b* may be concurrently modified by another thread. + .. c:function:: int PyDict_Update(PyObject *a, PyObject *b) @@ -347,6 +437,13 @@ Dictionary objects argument has no "keys" attribute. Return ``0`` on success or ``-1`` if an exception was raised. + .. note:: + + In the :term:`free-threaded build`, when *b* is a + :class:`dict` (with the standard iterator), both *a* and *b* are locked + for the duration of the operation. When *b* is a non-dict mapping, only + *a* is locked; *b* may be concurrently modified by another thread. + .. c:function:: int PyDict_MergeFromSeq2(PyObject *a, PyObject *seq2, int override) @@ -362,6 +459,13 @@ Dictionary objects if override or key not in a: a[key] = value + .. note:: + + In the :term:`free-threaded ` build, only *a* is locked. + The iteration over *seq2* is not synchronized; *seq2* may be concurrently + modified by another thread. + + .. c:function:: int PyDict_AddWatcher(PyDict_WatchCallback callback) Register *callback* as a dictionary watcher. Return a non-negative integer @@ -369,6 +473,13 @@ Dictionary objects of error (e.g. no more watcher IDs available), return ``-1`` and set an exception. + .. note:: + + This function is not internally synchronized. In the + :term:`free-threaded ` build, callers should ensure no + concurrent calls to :c:func:`PyDict_AddWatcher` or + :c:func:`PyDict_ClearWatcher` are in progress. + .. versionadded:: 3.12 .. c:function:: int PyDict_ClearWatcher(int watcher_id) @@ -377,6 +488,13 @@ Dictionary objects :c:func:`PyDict_AddWatcher`. Return ``0`` on success, ``-1`` on error (e.g. if the given *watcher_id* was never registered.) + .. note:: + + This function is not internally synchronized. In the + :term:`free-threaded ` build, callers should ensure no + concurrent calls to :c:func:`PyDict_AddWatcher` or + :c:func:`PyDict_ClearWatcher` are in progress. + .. versionadded:: 3.12 .. c:function:: int PyDict_Watch(int watcher_id, PyObject *dict) diff --git a/Doc/data/threadsafety.dat b/Doc/data/threadsafety.dat index f063ca1360d5fb..404f26ab2e8881 100644 --- a/Doc/data/threadsafety.dat +++ b/Doc/data/threadsafety.dat @@ -14,6 +14,64 @@ # The function name must match the C domain identifier used in the documentation. # Synchronization primitives (Doc/c-api/synchronization.rst) -PyMutex_Lock:shared: -PyMutex_Unlock:shared: +PyMutex_Lock:atomic: +PyMutex_Unlock:atomic: PyMutex_IsLocked:atomic: + +# Dictionary objects (Doc/c-api/dict.rst) + +# Type checks - read ob_type pointer, always safe +PyDict_Check:atomic: +PyDict_CheckExact:atomic: + +# Creation - pure allocation, no shared state +PyDict_New:atomic: + +# Lock-free lookups - use _Py_dict_lookup_threadsafe(), no locking +PyDict_Contains:atomic: +PyDict_ContainsString:atomic: +PyDict_GetItemRef:atomic: +PyDict_GetItemStringRef:atomic: +PyDict_Size:atomic: +PyDict_GET_SIZE:atomic: + +# Borrowed-reference lookups - lock-free dict access but returned +# borrowed reference is unsafe in free-threaded builds without +# external synchronization +PyDict_GetItem:compatible: +PyDict_GetItemWithError:compatible: +PyDict_GetItemString:compatible: +PyDict_SetDefault:compatible: + +# Iteration - no locking; returns borrowed refs +PyDict_Next:compatible: + +# Single-item mutations - protected by per-object critical section +PyDict_SetItem:shared: +PyDict_SetItemString:shared: +PyDict_DelItem:shared: +PyDict_DelItemString:shared: +PyDict_SetDefaultRef:shared: +PyDict_Pop:shared: +PyDict_PopString:shared: + +# Bulk reads - hold per-object lock for duration +PyDict_Clear:atomic: +PyDict_Copy:atomic: +PyDict_Keys:atomic: +PyDict_Values:atomic: +PyDict_Items:atomic: + +# Merge/update - lock target dict; also lock source when it is a dict +PyDict_Update:shared: +PyDict_Merge:shared: +PyDict_MergeFromSeq2:shared: + +# Watcher registration - no synchronization on interpreter state +PyDict_AddWatcher:compatible: +PyDict_ClearWatcher:compatible: + +# Per-dict watcher tags - non-atomic RMW on _ma_watcher_tag; +# safe on distinct dicts only +PyDict_Watch:distinct: +PyDict_Unwatch:distinct: