Skip to content

Commit dd05e67

Browse files
committed
gh-144809: Make deque copy atomic in free-threaded build
1 parent 3e2f5c1 commit dd05e67

File tree

3 files changed

+49
-0
lines changed

3 files changed

+49
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import unittest
2+
from collections import deque
3+
from copy import copy
4+
from test.support import threading_helper
5+
6+
threading_helper.requires_working_threading(module=True)
7+
8+
9+
class TestDeque(unittest.TestCase):
10+
def test_copy_race(self):
11+
# gh-144809: Test that deque copy is thread safe. It previously
12+
# could raise a "deque mutated during iteration" error.
13+
d = deque(range(100))
14+
15+
def mutate():
16+
i = 0
17+
for _ in range(1000):
18+
d.append(i)
19+
if len(d) > 200:
20+
d.popleft()
21+
i += 1
22+
23+
def copy_loop():
24+
for _ in range(1000):
25+
copy(d)
26+
27+
workers = [mutate, mutate, copy_loop, copy_loop]
28+
threading_helper.run_concurrently(workers)
29+
30+
31+
if __name__ == "__main__":
32+
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make :class:`collections.deque` copy atomic in the free-threaded build.

Modules/_collectionsmodule.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,6 +1983,22 @@ dequeiter_next(PyObject *op)
19831983
// It's safe to access it->deque without holding the per-object lock for it
19841984
// here; it->deque is only assigned during construction of it.
19851985
dequeobject *deque = it->deque;
1986+
1987+
#ifdef Py_GIL_DISABLED
1988+
// gh-144809: When called from deque_copy(), the deque is already
1989+
// locked. The two-object critical section below would unlock and
1990+
// re-lock the deque between calls, allowing another thread to modify
1991+
// it mid-iteration. The one-object critical section avoids this
1992+
// because it keeps the deque locked across calls when it's already
1993+
// held, due to a fast-path optimization.
1994+
if (_PyObject_IsUniquelyReferenced(it)) {
1995+
Py_BEGIN_CRITICAL_SECTION(deque);
1996+
result = dequeiter_next_lock_held(it, deque);
1997+
Py_END_CRITICAL_SECTION();
1998+
return result;
1999+
}
2000+
#endif
2001+
19862002
Py_BEGIN_CRITICAL_SECTION2(it, deque);
19872003
result = dequeiter_next_lock_held(it, deque);
19882004
Py_END_CRITICAL_SECTION2();

0 commit comments

Comments
 (0)