Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Lib/test/test_free_threading/test_collections.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make :class:`collections.deque` copy atomic in the free-threaded build.
16 changes: 16 additions & 0 deletions Modules/_collectionsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -1983,6 +1983,22 @@ dequeiter_next(PyObject *op)
// It's safe to access it->deque without holding the per-object lock for it
// here; it->deque is only assigned during construction of it.
dequeobject *deque = it->deque;

#ifdef Py_GIL_DISABLED
// gh-144809: When called from deque_copy(), the deque is already
// locked. The two-object critical section below would unlock and
// re-lock the deque between calls, allowing another thread to modify
// it mid-iteration. The one-object critical section avoids this
// because it keeps the deque locked across calls when it's already
// held, due to a fast-path optimization.
if (_PyObject_IsUniquelyReferenced((PyObject *)it)) {
Py_BEGIN_CRITICAL_SECTION(deque);
result = dequeiter_next_lock_held(it, deque);
Py_END_CRITICAL_SECTION();
return result;
}
#endif

Py_BEGIN_CRITICAL_SECTION2(it, deque);
result = dequeiter_next_lock_held(it, deque);
Py_END_CRITICAL_SECTION2();
Expand Down
Loading