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
118 changes: 118 additions & 0 deletions Doc/c-api/dict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,31 @@ 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)

This is the same as :c:func:`PyDict_SetItem`, but *key* is
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)

Expand All @@ -104,13 +122,31 @@ 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)

This is the same as :c:func:`PyDict_DelItem`, but *key* is
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)

Expand Down Expand Up @@ -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.
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -362,13 +459,27 @@ Dictionary objects
if override or key not in a:
a[key] = value

.. note::

In the :term:`free-threaded <free threading>` 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
id which must be passed to future calls to :c:func:`PyDict_Watch`. In case
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 <free threading>` 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)
Expand All @@ -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 <free threading>` 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)
Expand Down
62 changes: 60 additions & 2 deletions Doc/data/threadsafety.dat
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think these need to be annotated. PyDict_Check falls in the category of type checks, and reading type of a object is safe in free-threading in general, we can perhaps add this separately but it doesn't look useful to add this to every type check function

Copy link
Member Author

Choose a reason for hiding this comment

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

My view is more information is better than less. Since it's just one line in a .dat file and not something that will require a substantial maintenance effort, it's good to have it. Making things more easily discoverable is a strong argument.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm inclined with @lysnikolaou here

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:
Loading