Skip to content

Commit

Permalink
[3.13] gh-120858: PyDict_Next should not lock the dict (GH-120859) (#…
Browse files Browse the repository at this point in the history
…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 <colesbury@gmail.com>
  • Loading branch information
miss-islington and colesbury authored Jun 24, 2024
1 parent 6aee5ed commit 0a77058
Show file tree
Hide file tree
Showing 4 changed files with 34 additions and 8 deletions.
11 changes: 11 additions & 0 deletions Doc/c-api/dict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,17 @@ Dictionary Objects
Py_DECREF(o);
}
The function is not thread-safe in the :term:`free-threaded <free threading>`
build without external synchronization. You can use
:c:macro:`Py_BEGIN_CRITICAL_SECTION` to lock the dictionary while iterating
over it::
Py_BEGIN_CRITICAL_SECTION(self->dict);
while (PyDict_Next(self->dict, &pos, &key, &value)) {
...
}
Py_END_CRITICAL_SECTION();
.. c:function:: int PyDict_Merge(PyObject *a, PyObject *b, int override)
Expand Down
20 changes: 19 additions & 1 deletion Doc/howto/free-threading-extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ Containers like :c:struct:`PyListObject`,
in the free-threaded build. For example, the :c:func:`PyList_Append` will
lock the list before appending an item.

.. _PyDict_Next:

``PyDict_Next``
'''''''''''''''

A notable exception is :c:func:`PyDict_Next`, which does not lock the
dictionary. You should use :c:macro:`Py_BEGIN_CRITICAL_SECTION` to protect
the dictionary while iterating over it if the dictionary may be concurrently
modified::

Py_BEGIN_CRITICAL_SECTION(dict);
PyObject *key, *value;
Py_ssize_t pos = 0;
while (PyDict_Next(dict, &pos, &key, &value)) {
...
}
Py_END_CRITICAL_SECTION();


Borrowed References
===================
Expand Down Expand Up @@ -141,7 +159,7 @@ that return :term:`strong references <strong reference>`.
+-----------------------------------+-----------------------------------+
| :c:func:`PyDict_SetDefault` | :c:func:`PyDict_SetDefaultRef` |
+-----------------------------------+-----------------------------------+
| :c:func:`PyDict_Next` | no direct replacement |
| :c:func:`PyDict_Next` | none (see :ref:`PyDict_Next`) |
+-----------------------------------+-----------------------------------+
| :c:func:`PyWeakref_GetObject` | :c:func:`PyWeakref_GetRef` |
+-----------------------------------+-----------------------------------+
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:c:func:`PyDict_Next` no longer locks the dictionary in the free-threaded
build. The locking needs to be done by the caller around the entire iteration
loop.
8 changes: 1 addition & 7 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2801,8 +2801,6 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey,
if (!PyDict_Check(op))
return 0;

ASSERT_DICT_LOCKED(op);

mp = (PyDictObject *)op;
i = *ppos;
if (_PyDict_HasSplitTable(mp)) {
Expand Down Expand Up @@ -2875,11 +2873,7 @@ _PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey,
int
PyDict_Next(PyObject *op, Py_ssize_t *ppos, PyObject **pkey, PyObject **pvalue)
{
int res;
Py_BEGIN_CRITICAL_SECTION(op);
res = _PyDict_Next(op, ppos, pkey, pvalue, NULL);
Py_END_CRITICAL_SECTION();
return res;
return _PyDict_Next(op, ppos, pkey, pvalue, NULL);
}


Expand Down

0 comments on commit 0a77058

Please sign in to comment.