gh-123471: Make concurrent iteration over itertools.cycle safe under free-threading by eendebakpt · Pull Request #131212 · python/cpython · GitHub
Skip to content

gh-123471: Make concurrent iteration over itertools.cycle safe under free-threading - #131212

Merged
kumaraditya303 merged 10 commits into
python:mainfrom
eendebakpt:cycle_ft
Jun 2, 2025
Merged

gh-123471: Make concurrent iteration over itertools.cycle safe under free-threading#131212
kumaraditya303 merged 10 commits into
python:mainfrom
eendebakpt:cycle_ft

Conversation

@eendebakpt

@eendebakpt eendebakpt commented Mar 13, 2025

Copy link
Copy Markdown
Contributor

See #124397

  • In the FT build we cannot clear the iterator cycle->it. Instead we use cycle->index as a check whether the iterator has been exhausted or not.
  • We remove dead code related to cycle->firstpass
  • The unit test is combined with itertools.batched in a single file.

@rhettinger

Copy link
Copy Markdown
Contributor

@eendebakpt

Copy link
Copy Markdown
Contributor Author

The current implementation of itertools.cycle can crash under the free-threading build for two reasons:

  • Concurrent iteration can result in lz->index being incremented by a thread to a value of PyList_GET_SIZE(lz->saved)while in another thread the non thread-safePyList_GET_ITEM` is used.

item = PyList_GET_ITEM(lz->saved, lz->index);
lz->index++;
if (lz->index >= PyList_GET_SIZE(lz->saved))
lz->index = 0;

  • One thread can exhaust the iterator lz->it and clear it

Py_CLEAR(lz->it);
}

while another thread is still using lz->it (since the cycleobject might be holding the only reference to the iterator this is not safe). The test added in this PR will often crash the interpreter in the free-threading build on my system (increasing the number_of_iterations will increase the odds).

By "safe under free-threading" I mean the free-threading build will not crash. No guarantees are given about any "correctness" of the results (I can adapt the title and news entry if desired). It is true that using the planned serialize would be a good solution for someone planning to use the cycle with concurrent iteration. But I have not doubt some users will be using the itertools.cycle in a free-threading setting (perhaps even being unaware of this via 3rd party packages). I believe we should make the cpython free-threading safe against race conditions leading to crashes like this. These kind of bugs can be difficult to debug, and could potentially only trigger very rarely.

@rhettinger

rhettinger commented Mar 17, 2025

Copy link
Copy Markdown
Contributor

Okay, this sounds reasonable :-) Thanks for explaining the purpose of the edit.

Please do check that performance hasn't been negatively impacted (if so, the make an ifdef so that the baseline isn't impacted; if not, then we're good).

@eendebakpt

Copy link
Copy Markdown
Contributor Author

@rhettinger In the benchmarks I did this PR is faster for the normal build. There are two reasons for this

  • The unused variable firstpass is removed, making the cycleobject smaller in memory and avoiding the initialiation of the variable in the constructor. (note: this change is not related to FT)
  • Once the iterator is exhausted, we only access lz->index and lz->saved. In current main in addition the lz->it is accessed.

The gain from this PR is small though (and I would not be surprised if for different platforms or benchmark tests the gain is zero).

Here are the results of one of the benchmarks I did:

Benchmark code
import time
import gc
from itertools import cycle

t0=time.perf_counter()
for ii in range(100):
    c  = cycle( (1, 2, 3, 4))
    
    for _ in range(200):
        next(c)

gc.collect() # make sure that in both the normal and free-threading build we clean up the constructed objects
dt=time.perf_counter()-t0
print(dt)
Script to generate and plot the data
""" Interleaved benchmark for itertools.cycle

@author: eendebakpt
"""

import subprocess

import matplotlib.pyplot as plt
import numpy as np

test_script = '/home/eendebakpt/python_testing/benchmarks/bm_itertools_cycle.py'
cmds = ["/home/eendebakpt/cpython0/python {test_script", "/home/eendebakpt/cpython/python {test_script}" ]

verbose = False
tt = []
for ii in range(600):
    print(f"run {ii}")
    for cmd in cmds:
        p = subprocess.run(cmd, shell=True, check=True, capture_output=True, encoding="utf-8")
        if verbose:
            print(f"Command {p.args} exited with {p.returncode} code, output: \n{p.stdout}")
        tt.append(float(p.stdout))

tt_main = tt[::2]
tt_pr = tt[1::2]

# %% Show results
plt.figure(10)
plt.clf()
plt.plot(tt_main[::2], ".", label="Main")
plt.plot(tt_pr[1::2], ".", label="PR")
plt.axhline(np.mean(tt_main), color="C0", label="mean for main")
plt.axhline(np.mean(tt_pr), color="C1", label="mean for PR")
plt.ylabel("Execution time [s]")
plt.legend()

gain = np.mean(tt_main) / np.mean(tt_pr)
plt.title(f"Performance gain: {gain:.3f}")

image

@kumaraditya303
kumaraditya303 self-requested a review May 28, 2025 15:31
Comment thread Modules/itertoolsmodule.c Outdated
@kumaraditya303
kumaraditya303 merged commit 26a1cd4 into python:main Jun 2, 2025
@bedevere-bot

Copy link
Copy Markdown

Pranjal095 pushed a commit to Pranjal095/cpython that referenced this pull request Jul 12, 2025
…e under free-threading (python#131212)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
taegyunkim pushed a commit to taegyunkim/cpython that referenced this pull request Aug 4, 2025
…e under free-threading (python#131212)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Agent-Hellboy pushed a commit to Agent-Hellboy/cpython that referenced this pull request Aug 19, 2025
…e under free-threading (python#131212)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
nascheme added a commit that referenced this pull request Jul 17, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants