From e5fb1519fc4414f748823006c37e7882a814fdca Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 5 Feb 2026 21:15:06 +0100 Subject: [PATCH 1/5] Make itertools.islice safe in the FT build --- Modules/itertoolsmodule.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 7e73f76bc20b58..fd14cbd3a9c367 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() @@ -1620,33 +1621,39 @@ islice_next(PyObject *op) Py_ssize_t oldnext; PyObject *(*iternext)(PyObject *); - if (it == NULL) + Py_ssize_t cnt = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->cnt); + if (cnt < 0) return NULL; iternext = *Py_TYPE(it)->tp_iternext; - while (lz->cnt < lz->next) { + while (cnt < lz->next) { 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); + oldnext = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->next); /* 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; + FT_ATOMIC_STORE_SSIZE_RELAXED(lz->next, oldnext + (size_t)lz->step); + if (lz->next < oldnext || (stop != -1 && lz->next > stop)) { + FT_ATOMIC_STORE_SSIZE_RELAXED(lz->next, stop); + } return item; empty: + FT_ATOMIC_STORE_SSIZE_RELAXED(lz->cnt, -1); +#ifndef PY_GIL_DISABLED Py_CLEAR(lz->it); +#endif return NULL; } @@ -3555,7 +3562,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); From 6492f2feed947fe51937421f0cfdfff9f9aed677 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 5 Feb 2026 21:17:21 +0100 Subject: [PATCH 2/5] add test --- Lib/test/test_free_threading/test_itertools.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 From 443404e052f60afca6c244fb0d9f03eb227a0804 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 5 Feb 2026 21:31:25 +0100 Subject: [PATCH 3/5] atomic store of lz->next --- Modules/itertoolsmodule.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index fd14cbd3a9c367..d19dc40835b944 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -1643,8 +1643,9 @@ islice_next(PyObject *op) oldnext = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->next); /* The (size_t) cast below avoids the danger of undefined behaviour from signed integer overflow. */ - FT_ATOMIC_STORE_SSIZE_RELAXED(lz->next, oldnext + (size_t)lz->step); - if (lz->next < oldnext || (stop != -1 && lz->next > stop)) { + Py_ssize_t new_next = oldnext + (size_t)lz->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; From e1e61dff6b1ce2406e8e5b16eba9ad48d2dec18a Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Thu, 5 Feb 2026 21:37:18 +0100 Subject: [PATCH 4/5] use step as sentinel --- Modules/itertoolsmodule.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index d19dc40835b944..411c4f840962c8 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -1618,15 +1618,16 @@ islice_next(PyObject *op) PyObject *item; PyObject *it = lz->it; Py_ssize_t stop = lz->stop; - Py_ssize_t oldnext; PyObject *(*iternext)(PyObject *); - Py_ssize_t cnt = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->cnt); - if (cnt < 0) + 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 (cnt < lz->next) { + while (cnt < oldnext) { item = iternext(it); if (item == NULL) goto empty; @@ -1640,10 +1641,10 @@ islice_next(PyObject *op) goto empty; cnt++; FT_ATOMIC_STORE_SSIZE_RELAXED(lz->cnt, cnt); - oldnext = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->next); + /* The (size_t) cast below avoids the danger of undefined behaviour from signed integer overflow. */ - Py_ssize_t new_next = oldnext + (size_t)lz->step + 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); @@ -1651,7 +1652,7 @@ islice_next(PyObject *op) return item; empty: - FT_ATOMIC_STORE_SSIZE_RELAXED(lz->cnt, -1); + FT_ATOMIC_STORE_SSIZE_RELAXED(lz->step, -1); #ifndef PY_GIL_DISABLED Py_CLEAR(lz->it); #endif From c2cbffb6e28f80f021b0c0b915af0f136766fe3a Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:42:09 +0000 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-02-05-20-42-00.gh-issue-123471.Uj-Eyr.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-05-20-42-00.gh-issue-123471.Uj-Eyr.rst 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.