Skip to content

Commit 15d10a8

Browse files
authored
gh-142518: annotate dict C-APIs for thread safety (#145875)
1 parent 8f17140 commit 15d10a8

File tree

2 files changed

+157
-2
lines changed

2 files changed

+157
-2
lines changed

Doc/c-api/dict.rst

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ Dictionary objects
7676
7777
The first argument can be a :class:`dict` or a :class:`frozendict`.
7878
79+
.. note::
80+
81+
The operation is atomic on :term:`free threading <free-threaded build>`
82+
when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool` or :class:`bytes`.
83+
7984
.. versionchanged:: 3.15
8085
Also accept :class:`frozendict`.
8186
@@ -105,6 +110,11 @@ Dictionary objects
105110
``0`` on success or ``-1`` on failure. This function *does not* steal a
106111
reference to *val*.
107112
113+
.. note::
114+
115+
The operation is atomic on :term:`free threading <free-threaded build>`
116+
when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool` or :class:`bytes`.
117+
108118
109119
.. c:function:: int PyDict_SetItemString(PyObject *p, const char *key, PyObject *val)
110120
@@ -120,6 +130,11 @@ Dictionary objects
120130
If *key* is not in the dictionary, :exc:`KeyError` is raised.
121131
Return ``0`` on success or ``-1`` on failure.
122132
133+
.. note::
134+
135+
The operation is atomic on :term:`free threading <free-threaded build>`
136+
when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool` or :class:`bytes`.
137+
123138
124139
.. c:function:: int PyDict_DelItemString(PyObject *p, const char *key)
125140
@@ -140,6 +155,11 @@ Dictionary objects
140155
141156
The first argument can be a :class:`dict` or a :class:`frozendict`.
142157
158+
.. note::
159+
160+
The operation is atomic on :term:`free threading <free-threaded build>`
161+
when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool` or :class:`bytes`.
162+
143163
.. versionadded:: 3.13
144164
145165
.. versionchanged:: 3.15
@@ -162,6 +182,13 @@ Dictionary objects
162182
:meth:`~object.__eq__` methods are silently ignored.
163183
Prefer the :c:func:`PyDict_GetItemWithError` function instead.
164184
185+
.. note::
186+
187+
In the :term:`free-threaded build`, the returned
188+
:term:`borrowed reference` may become invalid if another thread modifies
189+
the dictionary concurrently. Prefer :c:func:`PyDict_GetItemRef`, which
190+
returns a :term:`strong reference`.
191+
165192
.. versionchanged:: 3.10
166193
Calling this API without an :term:`attached thread state` had been allowed for historical
167194
reason. It is no longer allowed.
@@ -177,6 +204,13 @@ Dictionary objects
177204
occurred. Return ``NULL`` **without** an exception set if the key
178205
wasn't present.
179206
207+
.. note::
208+
209+
In the :term:`free-threaded build`, the returned
210+
:term:`borrowed reference` may become invalid if another thread modifies
211+
the dictionary concurrently. Prefer :c:func:`PyDict_GetItemRef`, which
212+
returns a :term:`strong reference`.
213+
180214
.. versionchanged:: 3.15
181215
Also accept :class:`frozendict`.
182216
@@ -195,6 +229,13 @@ Dictionary objects
195229
Prefer using the :c:func:`PyDict_GetItemWithError` function with your own
196230
:c:func:`PyUnicode_FromString` *key* instead.
197231
232+
.. note::
233+
234+
In the :term:`free-threaded build`, the returned
235+
:term:`borrowed reference` may become invalid if another thread modifies
236+
the dictionary concurrently. Prefer :c:func:`PyDict_GetItemStringRef`,
237+
which returns a :term:`strong reference`.
238+
198239
.. versionchanged:: 3.15
199240
Also accept :class:`frozendict`.
200241
@@ -221,6 +262,14 @@ Dictionary objects
221262
222263
.. versionadded:: 3.4
223264
265+
.. note::
266+
267+
In the :term:`free-threaded build`, the returned
268+
:term:`borrowed reference` may become invalid if another thread modifies
269+
the dictionary concurrently. Prefer :c:func:`PyDict_SetDefaultRef`,
270+
which returns a :term:`strong reference`.
271+
272+
224273
225274
.. c:function:: int PyDict_SetDefaultRef(PyObject *p, PyObject *key, PyObject *default_value, PyObject **result)
226275
@@ -240,6 +289,11 @@ Dictionary objects
240289
These may refer to the same object: in that case you hold two separate
241290
references to it.
242291
292+
.. note::
293+
294+
The operation is atomic on :term:`free threading <free-threaded build>`
295+
when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool` or :class:`bytes`.
296+
243297
.. versionadded:: 3.13
244298
245299
@@ -257,6 +311,11 @@ Dictionary objects
257311
Similar to :meth:`dict.pop`, but without the default value and
258312
not raising :exc:`KeyError` if the key is missing.
259313
314+
.. note::
315+
316+
The operation is atomic on :term:`free threading <free-threaded build>`
317+
when *key* is :class:`str`, :class:`int`, :class:`float`, :class:`bool` or :class:`bytes`.
318+
260319
.. versionadded:: 3.13
261320
262321
@@ -403,6 +462,13 @@ Dictionary objects
403462
only be added if there is not a matching key in *a*. Return ``0`` on
404463
success or ``-1`` if an exception was raised.
405464
465+
.. note::
466+
467+
In the :term:`free-threaded build`, when *b* is a
468+
:class:`dict` (with the standard iterator), both *a* and *b* are locked
469+
for the duration of the operation. When *b* is a non-dict mapping, only
470+
*a* is locked; *b* may be concurrently modified by another thread.
471+
406472
407473
.. c:function:: int PyDict_Update(PyObject *a, PyObject *b)
408474
@@ -412,6 +478,13 @@ Dictionary objects
412478
argument has no "keys" attribute. Return ``0`` on success or ``-1`` if an
413479
exception was raised.
414480
481+
.. note::
482+
483+
In the :term:`free-threaded build`, when *b* is a
484+
:class:`dict` (with the standard iterator), both *a* and *b* are locked
485+
for the duration of the operation. When *b* is a non-dict mapping, only
486+
*a* is locked; *b* may be concurrently modified by another thread.
487+
415488
416489
.. c:function:: int PyDict_MergeFromSeq2(PyObject *a, PyObject *seq2, int override)
417490
@@ -427,13 +500,27 @@ Dictionary objects
427500
if override or key not in a:
428501
a[key] = value
429502
503+
.. note::
504+
505+
In the :term:`free-threaded <free threading>` build, only *a* is locked.
506+
The iteration over *seq2* is not synchronized; *seq2* may be concurrently
507+
modified by another thread.
508+
509+
430510
.. c:function:: int PyDict_AddWatcher(PyDict_WatchCallback callback)
431511
432512
Register *callback* as a dictionary watcher. Return a non-negative integer
433513
id which must be passed to future calls to :c:func:`PyDict_Watch`. In case
434514
of error (e.g. no more watcher IDs available), return ``-1`` and set an
435515
exception.
436516
517+
.. note::
518+
519+
This function is not internally synchronized. In the
520+
:term:`free-threaded <free threading>` build, callers should ensure no
521+
concurrent calls to :c:func:`PyDict_AddWatcher` or
522+
:c:func:`PyDict_ClearWatcher` are in progress.
523+
437524
.. versionadded:: 3.12
438525
439526
.. c:function:: int PyDict_ClearWatcher(int watcher_id)
@@ -442,6 +529,13 @@ Dictionary objects
442529
:c:func:`PyDict_AddWatcher`. Return ``0`` on success, ``-1`` on error (e.g.
443530
if the given *watcher_id* was never registered.)
444531
532+
.. note::
533+
534+
This function is not internally synchronized. In the
535+
:term:`free-threaded <free threading>` build, callers should ensure no
536+
concurrent calls to :c:func:`PyDict_AddWatcher` or
537+
:c:func:`PyDict_ClearWatcher` are in progress.
538+
445539
.. versionadded:: 3.12
446540
447541
.. c:function:: int PyDict_Watch(int watcher_id, PyObject *dict)

Doc/data/threadsafety.dat

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,71 @@
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:
2020

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

2384
# Type checks - read ob_type pointer, always safe

0 commit comments

Comments
 (0)