]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-144809: Make deque copy atomic in free-threaded build (gh-144966)
authorSam Gross <colesbury@gmail.com>
Fri, 20 Feb 2026 19:31:58 +0000 (14:31 -0500)
committerGitHub <noreply@github.com>
Fri, 20 Feb 2026 19:31:58 +0000 (14:31 -0500)
Lib/test/test_free_threading/test_collections.py [new file with mode: 0644]
Misc/NEWS.d/next/Library/2026-02-18-00-00-00.gh-issue-144809.nYpEUx.rst [new file with mode: 0644]
Modules/_collectionsmodule.c

diff --git a/Lib/test/test_free_threading/test_collections.py b/Lib/test/test_free_threading/test_collections.py
new file mode 100644 (file)
index 0000000..3a413cc
--- /dev/null
@@ -0,0 +1,29 @@
+import unittest
+from collections import deque
+from copy import copy
+from test.support import threading_helper
+
+threading_helper.requires_working_threading(module=True)
+
+
+class TestDeque(unittest.TestCase):
+    def test_copy_race(self):
+        # gh-144809: Test that deque copy is thread safe. It previously
+        # could raise a "deque mutated during iteration" error.
+        d = deque(range(100))
+
+        def mutate():
+            for i in range(1000):
+                d.append(i)
+                if len(d) > 200:
+                    d.popleft()
+
+        def copy_loop():
+            for _ in range(1000):
+                copy(d)
+
+        threading_helper.run_concurrently([mutate, copy_loop])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2026-02-18-00-00-00.gh-issue-144809.nYpEUx.rst b/Misc/NEWS.d/next/Library/2026-02-18-00-00-00.gh-issue-144809.nYpEUx.rst
new file mode 100644 (file)
index 0000000..62c20b7
--- /dev/null
@@ -0,0 +1 @@
+Make :class:`collections.deque` copy atomic in the :term:`free-threaded build`.
index 45ca63e6d7c77fcce7e16f6088c119942698ef8b..72865f87fc484f5748764429c6f5700c8889ff57 100644 (file)
@@ -605,29 +605,42 @@ deque_copy_impl(dequeobject *deque)
     collections_state *state = find_module_state_by_def(Py_TYPE(deque));
     if (Py_IS_TYPE(deque, state->deque_type)) {
         dequeobject *new_deque;
-        PyObject *rv;
+        Py_ssize_t n = Py_SIZE(deque);
 
         new_deque = (dequeobject *)deque_new(state->deque_type, NULL, NULL);
         if (new_deque == NULL)
             return NULL;
         new_deque->maxlen = old_deque->maxlen;
-        /* Fast path for the deque_repeat() common case where len(deque) == 1
-         *
-         * It's safe to not acquire the per-object lock for new_deque; it's
-         * invisible to other threads.
+
+        /* Copy elements directly by walking the block structure.
+         * This is safe because the caller holds the deque lock and
+         * the new deque is not yet visible to other threads.
          */
-        if (Py_SIZE(deque) == 1) {
-            PyObject *item = old_deque->leftblock->data[old_deque->leftindex];
-            rv = deque_append_impl(new_deque, item);
-        } else {
-            rv = deque_extend_impl(new_deque, (PyObject *)deque);
-        }
-        if (rv != NULL) {
-            Py_DECREF(rv);
-            return (PyObject *)new_deque;
+        if (n > 0) {
+            block *b = old_deque->leftblock;
+            Py_ssize_t index = old_deque->leftindex;
+
+            /* Space saving heuristic.  Start filling from the left */
+            assert(new_deque->leftblock == new_deque->rightblock);
+            assert(new_deque->leftindex == new_deque->rightindex + 1);
+            new_deque->leftindex = 1;
+            new_deque->rightindex = 0;
+
+            for (Py_ssize_t i = 0; i < n; i++) {
+                PyObject *item = b->data[index];
+                if (deque_append_lock_held(new_deque, Py_NewRef(item),
+                                           new_deque->maxlen) < 0) {
+                    Py_DECREF(new_deque);
+                    return NULL;
+                }
+                index++;
+                if (index == BLOCKLEN) {
+                    b = b->rightlink;
+                    index = 0;
+                }
+            }
         }
-        Py_DECREF(new_deque);
-        return NULL;
+        return (PyObject *)new_deque;
     }
     if (old_deque->maxlen < 0)
         result = PyObject_CallOneArg((PyObject *)(Py_TYPE(deque)),