Skip to content

Commit 0a77058

Browse files
[3.13] gh-120858: PyDict_Next should not lock the dict (GH-120859) (#120964)
PyDict_Next no longer locks the dictionary in the free-threaded build. Locking around individual PyDict_Next calls is not sufficient because the function returns borrowed references and because it allows concurrent modifications during the iteraiton loop. The internal locking also interferes with correct external synchronization because it may suspend outer critical sections created by the caller. (cherry picked from commit 375b723) Co-authored-by: Sam Gross <[email protected]>
1 parent 6aee5ed commit 0a77058

File tree

4 files changed

+34
-8
lines changed

4 files changed

+34
-8
lines changed

Doc/c-api/dict.rst

+11
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,17 @@ Dictionary Objects
290290
Py_DECREF(o);
291291
}
292292
293+
The function is not thread-safe in the :term:`free-threaded <free threading>`
294+
build without external synchronization. You can use
295+
:c:macro:`Py_BEGIN_CRITICAL_SECTION` to lock the dictionary while iterating
296+
over it::
297+
298+
Py_BEGIN_CRITICAL_SECTION(self->dict);
299+
while (PyDict_Next(self->dict, &pos, &key, &value)) {
300+
...
301+
}
302+
Py_END_CRITICAL_SECTION();
303+
293304
294305
.. c:function:: int PyDict_Merge(PyObject *a, PyObject *b, int override)
295306

Doc/howto/free-threading-extensions.rst

+19-1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,24 @@ Containers like :c:struct:`PyListObject`,
114114
in the free-threaded build. For example, the :c:func:`PyList_Append` will
115115
lock the list before appending an item.
116116

117+
.. _PyDict_Next:
118+
119+
``PyDict_Next``
120+
'''''''''''''''
121+
122+
A notable exception is :c:func:`PyDict_Next`, which does not lock the
123+
dictionary. You should use :c:macro:`Py_BEGIN_CRITICAL_SECTION` to protect
124+
the dictionary while iterating over it if the dictionary may be concurrently
125+
modified::
126+
127+
Py_BEGIN_CRITICAL_SECTION(dict);
128+
PyObject *key, *value;
129+
Py_ssize_t pos = 0;
130+
while (PyDict_Next(dict, &pos, &key, &value)) {
131+
...
132+
}
133+
Py_END_CRITICAL_SECTION();
134+
117135

118136
Borrowed References
119137
===================
@@ -141,7 +159,7 @@ that return :term:`strong references <strong reference>`.
141159
+-----------------------------------+-----------------------------------+
142160
| :c:func:`PyDict_SetDefault` | :c:func:`PyDict_SetDefaultRef` |
143161
+-----------------------------------+-----------------------------------+
144-
| :c:func:`PyDict_Next` | no direct replacement |
162+
| :c:func:`PyDict_Next` | none (see :ref:`PyDict_Next`) |
145163
+-----------------------------------+-----------------------------------+
146164
| :c:func:`PyWeakref_GetObject` | :c:func:`PyWeakref_GetRef` |
147165
+-----------------------------------+-----------------------------------+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:c:func:`PyDict_Next` no longer locks the dictionary in the free-threaded
2+
build. The locking needs to be done by the caller around the entire iteration
3+
loop.

Objects/dictobject.c

+1-7
Original file line numberDiff line numberDiff line change
@@ -2801,8 +2801,6 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey,
28012801
if (!PyDict_Check(op))
28022802
return 0;
28032803

2804-
ASSERT_DICT_LOCKED(op);
2805-
28062804
mp = (PyDictObject *)op;
28072805
i = *ppos;
28082806
if (_PyDict_HasSplitTable(mp)) {
@@ -2875,11 +2873,7 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey,
28752873
int
28762874
PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue)
28772875
{
2878-
int res;
2879-
Py_BEGIN_CRITICAL_SECTION(op);
2880-
res = _PyDict_Next(op, ppos, pkey, pvalue, NULL);
2881-
Py_END_CRITICAL_SECTION();
2882-
return res;
2876+
return _PyDict_Next(op, ppos, pkey, pvalue, NULL);
28832877
}
28842878

28852879

0 commit comments

Comments
 (0)