https://github.com/python/cpython/commit/009c8c052f5eb9f869c09029724ef194d8c161ca
commit: 009c8c052f5eb9f869c09029724ef194d8c161ca
branch: main
author: Pieter Eendebak <[email protected]>
committer: colesbury <[email protected]>
date: 2026-02-04T13:38:45-05:00
summary:

gh-123471: Make concurrent iteration over `itertools.permutations` and 
`itertools.combinations_with_replacement` thread-safe (gh-144402)

files:
A Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst
M Lib/test/test_free_threading/test_itertools.py
M Modules/itertoolsmodule.c

diff --git a/Lib/test/test_free_threading/test_itertools.py 
b/Lib/test/test_free_threading/test_itertools.py
index 9d366041917bb3..bb6047e8669475 100644
--- a/Lib/test/test_free_threading/test_itertools.py
+++ b/Lib/test/test_free_threading/test_itertools.py
@@ -1,94 +1,59 @@
 import unittest
-from threading import Thread, Barrier
-from itertools import batched, chain, cycle
+from itertools import batched, chain, combinations_with_replacement, cycle, 
permutations
 from test.support import threading_helper
 
 
 threading_helper.requires_working_threading(module=True)
 
-class ItertoolsThreading(unittest.TestCase):
-
-    @threading_helper.reap_threads
-    def test_batched(self):
-        number_of_threads = 10
-        number_of_iterations = 20
-        barrier = Barrier(number_of_threads)
-        def work(it):
-            barrier.wait()
-            while True:
-                try:
-                    next(it)
-                except StopIteration:
-                    break
 
-        data = tuple(range(1000))
-        for it in range(number_of_iterations):
-            batch_iterator = batched(data, 2)
-            worker_threads = []
-            for ii in range(number_of_threads):
-                worker_threads.append(
-                    Thread(target=work, args=[batch_iterator]))
+def work_iterator(it):
+    while True:
+        try:
+            next(it)
+        except StopIteration:
+            break
 
-            with threading_helper.start_threads(worker_threads):
-                pass
 
-            barrier.reset()
+class ItertoolsThreading(unittest.TestCase):
 
     @threading_helper.reap_threads
-    def test_cycle(self):
-        number_of_threads = 6
+    def test_batched(self):
         number_of_iterations = 10
-        number_of_cycles = 400
+        for _ in range(number_of_iterations):
+            it = batched(tuple(range(1000)), 2)
+            threading_helper.run_concurrently(work_iterator, nthreads=10, 
args=[it])
 
-        barrier = Barrier(number_of_threads)
+    @threading_helper.reap_threads
+    def test_cycle(self):
         def work(it):
-            barrier.wait()
-            for _ in range(number_of_cycles):
-                try:
-                    next(it)
-                except StopIteration:
-                    pass
+            for _ in range(400):
+                next(it)
 
-        data = (1, 2, 3, 4)
-        for it in range(number_of_iterations):
-            cycle_iterator = cycle(data)
-            worker_threads = []
-            for ii in range(number_of_threads):
-                worker_threads.append(
-                    Thread(target=work, args=[cycle_iterator]))
-
-            with threading_helper.start_threads(worker_threads):
-                pass
-
-            barrier.reset()
+        number_of_iterations = 6
+        for _ in range(number_of_iterations):
+            it = cycle((1, 2, 3, 4))
+            threading_helper.run_concurrently(work, nthreads=6, args=[it])
 
     @threading_helper.reap_threads
     def test_chain(self):
-        number_of_threads = 6
-        number_of_iterations = 20
-
-        barrier = Barrier(number_of_threads)
-        def work(it):
-            barrier.wait()
-            while True:
-                try:
-                    next(it)
-                except StopIteration:
-                    break
-
-        data = [(1, )] * 200
-        for it in range(number_of_iterations):
-            chain_iterator = chain(*data)
-            worker_threads = []
-            for ii in range(number_of_threads):
-                worker_threads.append(
-                    Thread(target=work, args=[chain_iterator]))
-
-            with threading_helper.start_threads(worker_threads):
-                pass
+        number_of_iterations = 10
+        for _ in range(number_of_iterations):
+            it = chain(*[(1,)] * 200)
+            threading_helper.run_concurrently(work_iterator, nthreads=6, 
args=[it])
 
-            barrier.reset()
+    @threading_helper.reap_threads
+    def test_combinations_with_replacement(self):
+        number_of_iterations = 6
+        for _ in range(number_of_iterations):
+            it = combinations_with_replacement(tuple(range(2)), 2)
+            threading_helper.run_concurrently(work_iterator, nthreads=6, 
args=[it])
 
+    @threading_helper.reap_threads
+    def test_permutations(self):
+        number_of_iterations = 6
+        for _ in range(number_of_iterations):
+            it = permutations(tuple(range(4)), 2)
+            threading_helper.run_concurrently(work_iterator, nthreads=6, 
args=[it])
 
 
 if __name__ == "__main__":
diff --git 
a/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst 
b/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst
new file mode 100644
index 00000000000000..85e9a03426e1fc
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-03-08-50-58.gh-issue-123471.yF1Gym.rst
@@ -0,0 +1 @@
+Make concurrent iteration over 
:class:`itertools.combinations_with_replacement` and 
:class:`itertools.permutations` safe under free-threading.
diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c
index 8685eff8be65c3..7e73f76bc20b58 100644
--- a/Modules/itertoolsmodule.c
+++ b/Modules/itertoolsmodule.c
@@ -2587,7 +2587,7 @@ cwr_traverse(PyObject *op, visitproc visit, void *arg)
 }
 
 static PyObject *
-cwr_next(PyObject *op)
+cwr_next_lock_held(PyObject *op)
 {
     cwrobject *co = cwrobject_CAST(op);
     PyObject *elem;
@@ -2666,6 +2666,16 @@ cwr_next(PyObject *op)
     return NULL;
 }
 
+static PyObject *
+cwr_next(PyObject *op)
+{
+    PyObject *result;
+    Py_BEGIN_CRITICAL_SECTION(op);
+    result = cwr_next_lock_held(op);
+    Py_END_CRITICAL_SECTION()
+    return result;
+}
+
 static PyMethodDef cwr_methods[] = {
     {"__sizeof__", cwr_sizeof, METH_NOARGS, sizeof_doc},
     {NULL,              NULL}   /* sentinel */
@@ -2846,7 +2856,7 @@ permutations_traverse(PyObject *op, visitproc visit, void 
*arg)
 }
 
 static PyObject *
-permutations_next(PyObject *op)
+permutations_next_lock_held(PyObject *op)
 {
     permutationsobject *po = permutationsobject_CAST(op);
     PyObject *elem;
@@ -2936,6 +2946,16 @@ permutations_next(PyObject *op)
     return NULL;
 }
 
+static PyObject *
+permutations_next(PyObject *op)
+{
+    PyObject *result;
+    Py_BEGIN_CRITICAL_SECTION(op);
+    result = permutations_next_lock_held(op);
+    Py_END_CRITICAL_SECTION()
+    return result;
+}
+
 static PyMethodDef permuations_methods[] = {
     {"__sizeof__", permutations_sizeof, METH_NOARGS, sizeof_doc},
     {NULL,              NULL}   /* sentinel */

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to