diff --git a/Lib/test/test_free_threading/test_itertools.py b/Lib/test/test_free_threading/test_itertools.py index bb6047e8669475..4772c83f598389 100644 --- a/Lib/test/test_free_threading/test_itertools.py +++ b/Lib/test/test_free_threading/test_itertools.py @@ -1,5 +1,5 @@ import unittest -from itertools import batched, chain, combinations_with_replacement, cycle, permutations +from itertools import batched, chain, combinations_with_replacement, cycle, islice, permutations from test.support import threading_helper @@ -48,6 +48,13 @@ def test_combinations_with_replacement(self): it = combinations_with_replacement(tuple(range(2)), 2) threading_helper.run_concurrently(work_iterator, nthreads=6, args=[it]) + @threading_helper.reap_threads + def test_islice(self): + number_of_iterations = 6 + for _ in range(number_of_iterations): + it = islice(tuple(range(10)), 1, 8, 2) + threading_helper.run_concurrently(work_iterator, nthreads=10, args=[it]) + @threading_helper.reap_threads def test_permutations(self): number_of_iterations = 6 diff --git a/Misc/NEWS.d/next/Library/2026-02-05-20-42-00.gh-issue-123471.Uj-Eyr.rst b/Misc/NEWS.d/next/Library/2026-02-05-20-42-00.gh-issue-123471.Uj-Eyr.rst new file mode 100644 index 00000000000000..52bc301b439943 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-05-20-42-00.gh-issue-123471.Uj-Eyr.rst @@ -0,0 +1 @@ +Make concurrent iterations over :class:`itertools.islice` safe under free-threading. diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 7e73f76bc20b58..411c4f840962c8 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -6,6 +6,7 @@ #include "pycore_moduleobject.h" // _PyModule_GetState() #include "pycore_typeobject.h" // _PyType_GetModuleState() #include "pycore_object.h" // _PyObject_GC_TRACK() +#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_STORE_SSIZE_RELAXED() #include "pycore_tuple.h" // _PyTuple_ITEMS() #include // offsetof() @@ -1617,36 +1618,44 @@ islice_next(PyObject *op) PyObject *item; PyObject *it = lz->it; Py_ssize_t stop = lz->stop; - Py_ssize_t oldnext; PyObject *(*iternext)(PyObject *); - if (it == NULL) + Py_ssize_t step = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->step); + if (step < 0) return NULL; + Py_ssize_t cnt = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->cnt); + Py_ssize_t oldnext = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->next); iternext = *Py_TYPE(it)->tp_iternext; - while (lz->cnt < lz->next) { + while (cnt < oldnext) { item = iternext(it); if (item == NULL) goto empty; Py_DECREF(item); - lz->cnt++; + cnt++; } - if (stop != -1 && lz->cnt >= stop) + if (stop != -1 && cnt >= stop) goto empty; item = iternext(it); if (item == NULL) goto empty; - lz->cnt++; - oldnext = lz->next; + cnt++; + FT_ATOMIC_STORE_SSIZE_RELAXED(lz->cnt, cnt); + /* The (size_t) cast below avoids the danger of undefined behaviour from signed integer overflow. */ - lz->next += (size_t)lz->step; - if (lz->next < oldnext || (stop != -1 && lz->next > stop)) - lz->next = stop; + Py_ssize_t new_next = oldnext + (size_t)step; + FT_ATOMIC_STORE_SSIZE_RELAXED(lz->next, new_next); + if (new_next < oldnext || (stop != -1 && new_next > stop)) { + FT_ATOMIC_STORE_SSIZE_RELAXED(lz->next, stop); + } return item; empty: + FT_ATOMIC_STORE_SSIZE_RELAXED(lz->step, -1); +#ifndef PY_GIL_DISABLED Py_CLEAR(lz->it); +#endif return NULL; } @@ -3555,7 +3564,7 @@ count_next(PyObject *op) PyObject *returned; Py_ssize_t cnt; - cnt = _Py_atomic_load_ssize_relaxed(&lz->cnt); + cnt = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->cnt); for (;;) { if (cnt == PY_SSIZE_T_MAX) { Py_BEGIN_CRITICAL_SECTION(lz);