Skip to content

Commit 202d1a8

Browse files
colesburyestyxx
authored andcommitted
pythongh-120858: PyDict_Next should not lock the dict (python#120859)
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.
1 parent d888779 commit 202d1a8

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
@@ -2808,8 +2808,6 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey,
28082808
if (!PyDict_Check(op))
28092809
return 0;
28102810

2811-
ASSERT_DICT_LOCKED(op);
2812-
28132811
mp = (PyDictObject *)op;
28142812
i = *ppos;
28152813
if (_PyDict_HasSplitTable(mp)) {
@@ -2882,11 +2880,7 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey,
28822880
int
28832881
PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue)
28842882
{
2885-
int res;
2886-
Py_BEGIN_CRITICAL_SECTION(op);
2887-
res = _PyDict_Next(op, ppos, pkey, pvalue, NULL);
2888-
Py_END_CRITICAL_SECTION();
2889-
return res;
2883+
return _PyDict_Next(op, ppos, pkey, pvalue, NULL);
28902884
}
28912885

28922886

0 commit comments

Comments
 (0)