Repository navigation
Make concurrent iteration over pairwise, combinations, permutations, cwr, product, etc. from itertools safe under free-threading #123471
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Aug 29, 2024 Can we talk about this at the sprint? I would like to have a sound overall strategy for how all of these should be approached (what guarantees can be made, what is most useful, decide whether to add locks, redesign from scratch or just provide an alternative code path, what is the least destabilizing changes that can be made, how close can be make this to generator equivalents, how to defend against reentrancy, how to not damage the stable existing implementation for non-freethreading builds, etc).
Also, I would like to start by evaluating
itertools.tee()which currently throws a 'RuntimeError` even on the non-freethreading build. The may be a useful behavior there that involves adding locks.Thinking just about
pairwise(), I not even sure what the desirable behavior would be (other than not crashing). Is there any legit use case for two threads to race non-deterministically for a feed of successive pairs. ISTM like this would almost always be the wrong thing to do. Perhaps raising a 'RuntimeError` like tee does would be the most useful thing to do.I definitely think we should not be creating PRs on this issue until we've made a conscious decision about the right overall approach.
@rhettinger Great idea. When making the PRs I already noticed it is hard to make trade-offs between the different implementation options. I was considering writing a message on DPO to get some more people involved, but having it discussed at the sprint first is also good. I won't be there, but would be interested in the outcome!
Reacted by Raymond HettingerNotes from the sprint are here: #124397 Strategy for Iterators in Free Threading
There is a simple Python implementation in the documentation. I believe it has sane behavior in multi-threading (not crash, not leak, not hangs). The C implementation should be equivalent. The current C implementation has some shortcuts for performance, they are optional and can be removed in the free-threading build.
For example, you can remove the result tuple caching in the free-threading build. It should not affect the behavior in most cases, it is simply an optimization. I believe that it is possible to use the same code for both builds if define macros for few elementary operations instead of wrapping larger parts of code in #ifdef/#endif.
But all tests that do not depend on race conditions should pass in both builds.
@serhiy @rhettinger For
itertools.pairwiseI created a new draft PR. Comments on the PR would be appreciated. In particular:- In the PR several ideas to address access to the cleared
po->itare mentioned. I picked the option with the easiest implementation. - For re-entrant usage of pairwise some special code was added to address bugs with borrowed references. In the free-threaded build in the PR we use normal references. I believe we can then remove the special code, although this will change the exact behavior. I left the special path in for now, but can remove it if you believe this simplifies the code.
- We can optimize (also for the GIL build)
if (_PyObject_IsUniquelyReferenced(result)) { Py_INCREF(result); PyObject *last_old = PyTuple_GET_ITEM(result, 0); PyObject *last_new = PyTuple_GET_ITEM(result, 1); PyTuple_SET_ITEM(result, 0, Py_NewRef(old)); PyTuple_SET_ITEM(result, 1, Py_NewRef(new)); Py_DECREF(last_old); Py_DECREF(last_new); // bpo-42536: The GC may have untracked this result tuple. Since we're // recycling it, make sure it's tracked again: if (!_PyObject_GC_IS_TRACKED(result)) { _PyObject_GC_TRACK(result); } } else { result = PyTuple_New(2); if (result != NULL) { PyTuple_SET_ITEM(result, 0, Py_NewRef(old)); PyTuple_SET_ITEM(result, 1, Py_NewRef(new)); } } Py_XSETREF(po->old, new); Py_DECREF(old); // instead of the decref here we could borrow the reference above Py_DECREF(it);into
if (_PyObject_IsUniquelyReferenced(result)) { Py_INCREF(result); PyObject *last_old = PyTuple_GET_ITEM(result, 0); PyObject *last_new = PyTuple_GET_ITEM(result, 1); PyTuple_SET_ITEM(result, 0, old); PyTuple_SET_ITEM(result, 1, Py_NewRef(new)); Py_DECREF(last_old); Py_DECREF(last_new); // bpo-42536: The GC may have untracked this result tuple. Since we're // recycling it, make sure it's tracked again: if (!_PyObject_GC_IS_TRACKED(result)) { _PyObject_GC_TRACK(result); } } else { result = PyTuple_New(2); if (result != NULL) { PyTuple_SET_ITEM(result, 0, old); PyTuple_SET_ITEM(result, 1, Py_NewRef(new)); } else { Py_DECREF(old); } } Py_XSETREF(po->old, new); Py_DECREF(it);This saves an incref/decref in the common case. Is this worthwhile to add?
- In the PR several ideas to address access to the cleared
- changed the title
[-]Make concurrent iteration over pairwise, combinations, permutations, cwr, product from itertools safe under free-threading[/-][+]Make concurrent iteration over pairwise, combinations, permutations, cwr, product, etc. from itertools safe under free-threading[/+]on Jan 28, 2025 - addedextension-modulesC modules in the Modules dirC modules in the Modules dir
on Mar 14, 2025 - added a commit that references this issue
on Apr 13, 2025 16 remaining items
- added a commit that references this issue
on Feb 4, 2026 - added a commit that references this issue
on Mar 16, 2026
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsTodo
Bug report
Bug description:
Several methods from the C implementation of the itertools module are not yet safe to use under the free-threading build. In this issue we list several issues to be addressed. The issues below are discussed for
itertools.product, but the issues are similar for the other classes.When iterating over
productthe result tuple is re-used when the reference count is 1. We can use the new_PyObject_IsUniquelyReferencedmethod to perform the check whether we can re-use the tuple. (this issue was also reported in enum_next and pairwise_next can result in tuple elements with zero reference count in free-threading build #121464)On the first invocation of
producta new result is constructed.cpython/Modules/itertoolsmodule.c
Lines 2038 to 2044 in 58ce131
This is not thread-safe, as multiple threads could have
result == NULLevaluate to true. We could move the construction of theproductobject.resultto the constructor ofproduct. This does mean thatproductwill use more memory before the first invocation ofnext. This seems to be acceptable, as constructing aproductwithout iterating over it seems rare in practice.The tuple also needs to be filled with data. For
productit seems safe to do this in the constructor, as the data is comingfrom
productobject->poolswhich is a tuple of tuples. But forpairwisethe data is coming from an iterablecpython/Modules/itertoolsmodule.c
Lines 337 to 343 in 58ce131
which could be a generator. Reading data from the iterator before the first invocation of
pairwise_nextseems like a behavior change we do not want to make.An alternative is to use some kind of locking inside
product_next, but the locking should not add any overhead in the common path otherwise the single-thread performance will suffer.pairwise_nextatcpython/Modules/itertoolsmodule.c
Lines 352 to 356 in 58ce131
This cleaning up is not safe in concurrent iteration. Instead we can defer the cleaning up untill the object itself is decallocated (this approach was used for
reversed, see https://github.com/python/cpython/pull/120971/files#r1653313765)cpython/Modules/itertoolsmodule.c
Lines 2077 to 2088 in 58ce131
If two threads both increment
indices[i]the check on line 2078 is never true end we end up indexingpoolwithPyTuple_GET_ITEMoutside the bounds on line 2088. Here we could change the check intoindices[i] >= PyTuple_GET_SIZE(pool). That is equivalent for the single-threaded case, but does not lead to out-of-bounds indexing in the multi-threaded case (although it does lead to funny results!)@rhettinger @colesbury Any input on the points above would be welcome.
CPython versions tested on:
CPython main branch
Operating systems tested on:
No response
Linked PRs