gh-123471: Make itertools.chain thread-safe - #135689
Conversation
| barrier.wait() | ||
| while True: | ||
| try: | ||
| _ = next(it) |
There was a problem hiding this comment.
Why the extra assignment to _?
There was a problem hiding this comment.
Some linters complain if the value is not assigned. I can remove it though, it is not needed here.
There was a problem hiding this comment.
Linters tend to complain about a lot of things in the source. I'd just remove it.
The lock is a critical section. It's completely compiled away on the normal build, so there shouldn't be any overhead. If you're worried about the extra nested function call, I'm pretty sure that the compiler will inline it.
Yeah, there's a bit of a double-standard here. Basically:
It's not totally clear to me where |
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
kumaraditya303
left a comment
There was a problem hiding this comment.
LGTM
The critical sections macros do nothing on the default gil enabled build so there is no overhead. It only adds locking on free-threading builds.
…132814) (GH-135689) (GH-144402) (GH-146033) (GH-142957) (GH-153791) Combined backport of PRs from main branch, fixing free-threading data-races in itertools: * gh-123471: make concurrent iteration over `itertools.cycle` safe under free-threading (gh-131212) * gh-123471: Make itertools.product and itertools.combinations thread-safe (GH-132814) * gh-123471: Make itertools.chain thread-safe (gh-135689) * gh-123471: Make concurrent iteration over `itertools.permutations` and `itertools.combinations_with_replacement` thread-safe (gh-144402) * gh-123471: make concurrent iteration over itertools.accumulate thread-safe (gh-144486) * gh-123471: Make `itertools.zip_longest` safe in the FT build (gh-146033) (cherry picked from commit 26a1cd4) (cherry picked from commit 847d1c2) (cherry picked from commit 0533c1f) (cherry picked from commit 009c8c0) (cherry picked from commit 3a24856) (cherry picked from commit 9214e3f) Co-authored-by: Pieter Eendebak <pieter.eendebak@gmail.com> Co-authored-by: Kumar Aditya <kumaraditya@python.org>

The
itertools.chainhas two attributes that are mutated duration iteration, making it non-thread safe.Options to make it thread safe
i) Use a lock (simple, but adds a performance penalty for the single-threaded case)
ii) Make the iterator thread safe using atomics.
For the second option we can stop clearing the
lz->sourceon exhaustion (this is a standard mitigation to avoid settinglz->sourceto zero (and doing a decref) while another thread is still usinglz->source). To signal the iterator thatlz->sourceis exhausted we can either add a new attribute, or replace thewhile (lz->source != NULL)withwhile (true). This creates a slower path the the iterator is exhausted, but typically once the iterator is exhausted performance is not relevant any longer. Thelz->activeis more problematic. Oncelz->activeis exhausted, we need to update this with a new one (e.g.PyIter_Next(lz->source). Butlz->activeonly has a single reference count. So updating in one thread means having to decref the oldlz->active. Possible mitigations: a) atomically load-and-incref thelz->activeinchain_nextb) add a new attribute that contains all the exhausted values oflz->activeand clear this once thechainobjectitself is deallocated.The second option is complex and has some performance issues (e.g. more incref/decrefs, keeping references to exhausted iterators longer in memory), so in this PR we pick the first option.