Skip to content

Commit 52297b7

Browse files
committed
gh-142518: Annotate PyDict_* C APIs for thread safety
1 parent 7836ecc commit 52297b7

File tree

2 files changed

+178
-2
lines changed

2 files changed

+178
-2
lines changed

Doc/c-api/dict.rst

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,31 @@ Dictionary objects
8989
``0`` on success or ``-1`` on failure. This function *does not* steal a
9090
reference to *val*.
9191
92+
.. note::
93+
94+
In the :term:`free-threaded build`, key hashing via
95+
:meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__`
96+
can execute arbitrary Python code, during which the :term:`per-object
97+
lock` may be temporarily released. For built-in key types
98+
(:class:`str`, :class:`int`, :class:`float`), the lock is not released
99+
during comparison.
100+
92101
93102
.. c:function:: int PyDict_SetItemString(PyObject *p, const char *key, PyObject *val)
94103
95104
This is the same as :c:func:`PyDict_SetItem`, but *key* is
96105
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
97106
rather than a :c:expr:`PyObject*`.
98107
108+
.. note::
109+
110+
In the :term:`free-threaded build`, key hashing via
111+
:meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__`
112+
can execute arbitrary Python code, during which the :term:`per-object
113+
lock` may be temporarily released. For built-in key types
114+
(:class:`str`, :class:`int`, :class:`float`), the lock is not released
115+
during comparison.
116+
99117
100118
.. c:function:: int PyDict_DelItem(PyObject *p, PyObject *key)
101119
@@ -104,13 +122,31 @@ Dictionary objects
104122
If *key* is not in the dictionary, :exc:`KeyError` is raised.
105123
Return ``0`` on success or ``-1`` on failure.
106124
125+
.. note::
126+
127+
In the :term:`free-threaded build`, key hashing via
128+
:meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__`
129+
can execute arbitrary Python code, during which the :term:`per-object
130+
lock` may be temporarily released. For built-in key types
131+
(:class:`str`, :class:`int`, :class:`float`), the lock is not released
132+
during comparison.
133+
107134
108135
.. c:function:: int PyDict_DelItemString(PyObject *p, const char *key)
109136
110137
This is the same as :c:func:`PyDict_DelItem`, but *key* is
111138
specified as a :c:expr:`const char*` UTF-8 encoded bytes string,
112139
rather than a :c:expr:`PyObject*`.
113140
141+
.. note::
142+
143+
In the :term:`free-threaded build`, key hashing via
144+
:meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__`
145+
can execute arbitrary Python code, during which the :term:`per-object
146+
lock` may be temporarily released. For built-in key types
147+
(:class:`str`, :class:`int`, :class:`float`), the lock is not released
148+
during comparison.
149+
114150
115151
.. c:function:: int PyDict_GetItemRef(PyObject *p, PyObject *key, PyObject **result)
116152
@@ -139,6 +175,13 @@ Dictionary objects
139175
:meth:`~object.__eq__` methods are silently ignored.
140176
Prefer the :c:func:`PyDict_GetItemWithError` function instead.
141177
178+
.. note::
179+
180+
In the :term:`free-threaded build`, the returned
181+
:term:`borrowed reference` may become invalid if another thread modifies
182+
the dictionary concurrently. Prefer :c:func:`PyDict_GetItemRef`, which
183+
returns a :term:`strong reference`.
184+
142185
.. versionchanged:: 3.10
143186
Calling this API without an :term:`attached thread state` had been allowed for historical
144187
reason. It is no longer allowed.
@@ -151,6 +194,13 @@ Dictionary objects
151194
occurred. Return ``NULL`` **without** an exception set if the key
152195
wasn't present.
153196
197+
.. note::
198+
199+
In the :term:`free-threaded build`, the returned
200+
:term:`borrowed reference` may become invalid if another thread modifies
201+
the dictionary concurrently. Prefer :c:func:`PyDict_GetItemRef`, which
202+
returns a :term:`strong reference`.
203+
154204
155205
.. c:function:: PyObject* PyDict_GetItemString(PyObject *p, const char *key)
156206
@@ -166,6 +216,13 @@ Dictionary objects
166216
Prefer using the :c:func:`PyDict_GetItemWithError` function with your own
167217
:c:func:`PyUnicode_FromString` *key* instead.
168218
219+
.. note::
220+
221+
In the :term:`free-threaded build`, the returned
222+
:term:`borrowed reference` may become invalid if another thread modifies
223+
the dictionary concurrently. Prefer :c:func:`PyDict_GetItemStringRef`,
224+
which returns a :term:`strong reference`.
225+
169226
170227
.. c:function:: int PyDict_GetItemStringRef(PyObject *p, const char *key, PyObject **result)
171228
@@ -186,6 +243,14 @@ Dictionary objects
186243
187244
.. versionadded:: 3.4
188245
246+
.. note::
247+
248+
In the :term:`free-threaded build`, the returned
249+
:term:`borrowed reference` may become invalid if another thread modifies
250+
the dictionary concurrently. Prefer :c:func:`PyDict_SetDefaultRef`,
251+
which returns a :term:`strong reference`.
252+
253+
189254
190255
.. c:function:: int PyDict_SetDefaultRef(PyObject *p, PyObject *key, PyObject *default_value, PyObject **result)
191256
@@ -224,6 +289,15 @@ Dictionary objects
224289
225290
.. versionadded:: 3.13
226291
292+
.. note::
293+
294+
In the :term:`free-threaded build`, key hashing via
295+
:meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__`
296+
can execute arbitrary Python code, during which the :term:`per-object
297+
lock` may be temporarily released. For built-in key types
298+
(:class:`str`, :class:`int`, :class:`float`), the lock is not released
299+
during comparison.
300+
227301
228302
.. c:function:: int PyDict_PopString(PyObject *p, const char *key, PyObject **result)
229303
@@ -233,6 +307,15 @@ Dictionary objects
233307
234308
.. versionadded:: 3.13
235309
310+
.. note::
311+
312+
In the :term:`free-threaded build`, key hashing via
313+
:meth:`~object.__hash__` and key comparison via :meth:`~object.__eq__`
314+
can execute arbitrary Python code, during which the :term:`per-object
315+
lock` may be temporarily released. For built-in key types
316+
(:class:`str`, :class:`int`, :class:`float`), the lock is not released
317+
during comparison.
318+
236319
237320
.. c:function:: PyObject* PyDict_Items(PyObject *p)
238321
@@ -338,6 +421,13 @@ Dictionary objects
338421
only be added if there is not a matching key in *a*. Return ``0`` on
339422
success or ``-1`` if an exception was raised.
340423
424+
.. note::
425+
426+
In the :term:`free-threaded build`, when *b* is a
427+
:class:`dict` (with the standard iterator), both *a* and *b* are locked
428+
for the duration of the operation. When *b* is a non-dict mapping, only
429+
*a* is locked; *b* may be concurrently modified by another thread.
430+
341431
342432
.. c:function:: int PyDict_Update(PyObject *a, PyObject *b)
343433
@@ -347,6 +437,13 @@ Dictionary objects
347437
argument has no "keys" attribute. Return ``0`` on success or ``-1`` if an
348438
exception was raised.
349439
440+
.. note::
441+
442+
In the :term:`free-threaded build`, when *b* is a
443+
:class:`dict` (with the standard iterator), both *a* and *b* are locked
444+
for the duration of the operation. When *b* is a non-dict mapping, only
445+
*a* is locked; *b* may be concurrently modified by another thread.
446+
350447
351448
.. c:function:: int PyDict_MergeFromSeq2(PyObject *a, PyObject *seq2, int override)
352449
@@ -362,13 +459,27 @@ Dictionary objects
362459
if override or key not in a:
363460
a[key] = value
364461
462+
.. note::
463+
464+
In the :term:`free-threaded <free threading>` build, only *a* is locked.
465+
The iteration over *seq2* is not synchronized; *seq2* may be concurrently
466+
modified by another thread.
467+
468+
365469
.. c:function:: int PyDict_AddWatcher(PyDict_WatchCallback callback)
366470
367471
Register *callback* as a dictionary watcher. Return a non-negative integer
368472
id which must be passed to future calls to :c:func:`PyDict_Watch`. In case
369473
of error (e.g. no more watcher IDs available), return ``-1`` and set an
370474
exception.
371475
476+
.. note::
477+
478+
This function is not internally synchronized. In the
479+
:term:`free-threaded <free threading>` build, callers should ensure no
480+
concurrent calls to :c:func:`PyDict_AddWatcher` or
481+
:c:func:`PyDict_ClearWatcher` are in progress.
482+
372483
.. versionadded:: 3.12
373484
374485
.. c:function:: int PyDict_ClearWatcher(int watcher_id)
@@ -377,6 +488,13 @@ Dictionary objects
377488
:c:func:`PyDict_AddWatcher`. Return ``0`` on success, ``-1`` on error (e.g.
378489
if the given *watcher_id* was never registered.)
379490
491+
.. note::
492+
493+
This function is not internally synchronized. In the
494+
:term:`free-threaded <free threading>` build, callers should ensure no
495+
concurrent calls to :c:func:`PyDict_AddWatcher` or
496+
:c:func:`PyDict_ClearWatcher` are in progress.
497+
380498
.. versionadded:: 3.12
381499
382500
.. c:function:: int PyDict_Watch(int watcher_id, PyObject *dict)

Doc/data/threadsafety.dat

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,64 @@
1414
# The function name must match the C domain identifier used in the documentation.
1515

1616
# Synchronization primitives (Doc/c-api/synchronization.rst)
17-
PyMutex_Lock:shared:
18-
PyMutex_Unlock:shared:
17+
PyMutex_Lock:atomic:
18+
PyMutex_Unlock:atomic:
1919
PyMutex_IsLocked:atomic:
20+
21+
# Dictionary objects (Doc/c-api/dict.rst)
22+
23+
# Type checks - read ob_type pointer, always safe
24+
PyDict_Check:atomic:
25+
PyDict_CheckExact:atomic:
26+
27+
# Creation - pure allocation, no shared state
28+
PyDict_New:atomic:
29+
30+
# Lock-free lookups - use _Py_dict_lookup_threadsafe(), no locking
31+
PyDict_Contains:atomic:
32+
PyDict_ContainsString:atomic:
33+
PyDict_GetItemRef:atomic:
34+
PyDict_GetItemStringRef:atomic:
35+
PyDict_Size:atomic:
36+
PyDict_GET_SIZE:atomic:
37+
38+
# Borrowed-reference lookups - lock-free dict access but returned
39+
# borrowed reference is unsafe in free-threaded builds without
40+
# external synchronization
41+
PyDict_GetItem:compatible:
42+
PyDict_GetItemWithError:compatible:
43+
PyDict_GetItemString:compatible:
44+
PyDict_SetDefault:compatible:
45+
46+
# Iteration - no locking; returns borrowed refs
47+
PyDict_Next:compatible:
48+
49+
# Single-item mutations - protected by per-object critical section
50+
PyDict_SetItem:shared:
51+
PyDict_SetItemString:shared:
52+
PyDict_DelItem:shared:
53+
PyDict_DelItemString:shared:
54+
PyDict_SetDefaultRef:shared:
55+
PyDict_Pop:shared:
56+
PyDict_PopString:shared:
57+
58+
# Bulk reads - hold per-object lock for duration
59+
PyDict_Clear:atomic:
60+
PyDict_Copy:atomic:
61+
PyDict_Keys:atomic:
62+
PyDict_Values:atomic:
63+
PyDict_Items:atomic:
64+
65+
# Merge/update - lock target dict; also lock source when it is a dict
66+
PyDict_Update:shared:
67+
PyDict_Merge:shared:
68+
PyDict_MergeFromSeq2:shared:
69+
70+
# Watcher registration - no synchronization on interpreter state
71+
PyDict_AddWatcher:compatible:
72+
PyDict_ClearWatcher:compatible:
73+
74+
# Per-dict watcher tags - non-atomic RMW on _ma_watcher_tag;
75+
# safe on distinct dicts only
76+
PyDict_Watch:distinct:
77+
PyDict_Unwatch:distinct:

0 commit comments

Comments
 (0)