Skip to content

Commit 26a1cd4

Browse files
gh-123471: make concurrent iteration over itertools.cycle safe under free-threading (#131212)
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
1 parent b6237c3 commit 26a1cd4

File tree

3 files changed

+46
-14
lines changed

3 files changed

+46
-14
lines changed

Lib/test/test_free_threading/test_itertools_batched.py renamed to Lib/test/test_free_threading/test_itertools.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import unittest
22
from threading import Thread, Barrier
3-
from itertools import batched
3+
from itertools import batched, cycle
44
from test.support import threading_helper
55

66

77
threading_helper.requires_working_threading(module=True)
88

9-
class EnumerateThreading(unittest.TestCase):
9+
class ItertoolsThreading(unittest.TestCase):
1010

1111
@threading_helper.reap_threads
12-
def test_threading(self):
12+
def test_batched(self):
1313
number_of_threads = 10
1414
number_of_iterations = 20
1515
barrier = Barrier(number_of_threads)
@@ -34,5 +34,31 @@ def work(it):
3434

3535
barrier.reset()
3636

37+
@threading_helper.reap_threads
38+
def test_cycle(self):
39+
number_of_threads = 6
40+
number_of_iterations = 10
41+
number_of_cycles = 400
42+
43+
barrier = Barrier(number_of_threads)
44+
def work(it):
45+
barrier.wait()
46+
for _ in range(number_of_cycles):
47+
_ = next(it)
48+
49+
data = (1, 2, 3, 4)
50+
for it in range(number_of_iterations):
51+
cycle_iterator = cycle(data)
52+
worker_threads = []
53+
for ii in range(number_of_threads):
54+
worker_threads.append(
55+
Thread(target=work, args=[cycle_iterator]))
56+
57+
with threading_helper.start_threads(worker_threads):
58+
pass
59+
60+
barrier.reset()
61+
62+
3763
if __name__ == "__main__":
3864
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make concurrent iterations over :class:`itertools.cycle` safe under free-threading.

Modules/itertoolsmodule.c

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,6 @@ typedef struct {
11241124
PyObject *it;
11251125
PyObject *saved;
11261126
Py_ssize_t index;
1127-
int firstpass;
11281127
} cycleobject;
11291128

11301129
#define cycleobject_CAST(op) ((cycleobject *)(op))
@@ -1165,8 +1164,7 @@ itertools_cycle_impl(PyTypeObject *type, PyObject *iterable)
11651164
}
11661165
lz->it = it;
11671166
lz->saved = saved;
1168-
lz->index = 0;
1169-
lz->firstpass = 0;
1167+
lz->index = -1;
11701168

11711169
return (PyObject *)lz;
11721170
}
@@ -1199,11 +1197,11 @@ cycle_next(PyObject *op)
11991197
cycleobject *lz = cycleobject_CAST(op);
12001198
PyObject *item;
12011199

1202-
if (lz->it != NULL) {
1200+
Py_ssize_t index = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->index);
1201+
1202+
if (index < 0) {
12031203
item = PyIter_Next(lz->it);
12041204
if (item != NULL) {
1205-
if (lz->firstpass)
1206-
return item;
12071205
if (PyList_Append(lz->saved, item)) {
12081206
Py_DECREF(item);
12091207
return NULL;
@@ -1213,15 +1211,22 @@ cycle_next(PyObject *op)
12131211
/* Note: StopIteration is already cleared by PyIter_Next() */
12141212
if (PyErr_Occurred())
12151213
return NULL;
1214+
index = 0;
1215+
FT_ATOMIC_STORE_SSIZE_RELAXED(lz->index, 0);
1216+
#ifndef Py_GIL_DISABLED
12161217
Py_CLEAR(lz->it);
1218+
#endif
12171219
}
12181220
if (PyList_GET_SIZE(lz->saved) == 0)
12191221
return NULL;
1220-
item = PyList_GET_ITEM(lz->saved, lz->index);
1221-
lz->index++;
1222-
if (lz->index >= PyList_GET_SIZE(lz->saved))
1223-
lz->index = 0;
1224-
return Py_NewRef(item);
1222+
item = PyList_GetItemRef(lz->saved, index);
1223+
assert(item);
1224+
index++;
1225+
if (index >= PyList_GET_SIZE(lz->saved)) {
1226+
index = 0;
1227+
}
1228+
FT_ATOMIC_STORE_SSIZE_RELAXED(lz->index, index);
1229+
return item;
12251230
}
12261231

12271232
static PyType_Slot cycle_slots[] = {

0 commit comments

Comments
 (0)