Skip to content

Commit 375b723

Browse files
authored
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 dee63cb commit 375b723

File tree

4 files changed

+34
-8
lines changed

4 files changed

+34
-8
lines changed

Doc/c-api/dict.rst

Lines changed: 11 additions & 0 deletions
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

Lines changed: 19 additions & 1 deletion
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
+-----------------------------------+-----------------------------------+
Lines changed: 3 additions & 0 deletions
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

Lines changed: 1 addition & 7 deletions
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)