]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-129068: Make range iterators thread-safe (gh-142886)
authorSam Gross <colesbury@gmail.com>
Thu, 18 Dec 2025 18:11:51 +0000 (13:11 -0500)
committerGitHub <noreply@github.com>
Thu, 18 Dec 2025 18:11:51 +0000 (13:11 -0500)
Now that we specialize range iteration in the interpreter for the common
case where the iterator has only one reference, there's not a
significant performance cost to making the iteration thread-safe.

Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-10-49-03.gh-issue-129068.GlYnrO.rst [new file with mode: 0644]
Objects/clinic/rangeobject.c.h [new file with mode: 0644]
Objects/rangeobject.c
Tools/tsan/suppressions_free_threading.txt

diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-10-49-03.gh-issue-129068.GlYnrO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-10-49-03.gh-issue-129068.GlYnrO.rst
new file mode 100644 (file)
index 0000000..16b1952
--- /dev/null
@@ -0,0 +1,2 @@
+Make concurrent iteration over the same range iterator thread-safe in the
+free threading build.
diff --git a/Objects/clinic/rangeobject.c.h b/Objects/clinic/rangeobject.c.h
new file mode 100644 (file)
index 0000000..d9142ed
--- /dev/null
@@ -0,0 +1,150 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION()
+
+PyDoc_STRVAR(range_iterator___length_hint____doc__,
+"__length_hint__($self, /)\n"
+"--\n"
+"\n"
+"Private method returning an estimate of len(list(it)).");
+
+#define RANGE_ITERATOR___LENGTH_HINT___METHODDEF    \
+    {"__length_hint__", (PyCFunction)range_iterator___length_hint__, METH_NOARGS, range_iterator___length_hint____doc__},
+
+static PyObject *
+range_iterator___length_hint___impl(_PyRangeIterObject *r);
+
+static PyObject *
+range_iterator___length_hint__(PyObject *r, PyObject *Py_UNUSED(ignored))
+{
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(r);
+    return_value = range_iterator___length_hint___impl((_PyRangeIterObject *)r);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
+}
+
+PyDoc_STRVAR(range_iterator___reduce____doc__,
+"__reduce__($self, /)\n"
+"--\n"
+"\n"
+"Return state information for pickling.");
+
+#define RANGE_ITERATOR___REDUCE___METHODDEF    \
+    {"__reduce__", (PyCFunction)range_iterator___reduce__, METH_NOARGS, range_iterator___reduce____doc__},
+
+static PyObject *
+range_iterator___reduce___impl(_PyRangeIterObject *r);
+
+static PyObject *
+range_iterator___reduce__(PyObject *r, PyObject *Py_UNUSED(ignored))
+{
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(r);
+    return_value = range_iterator___reduce___impl((_PyRangeIterObject *)r);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
+}
+
+PyDoc_STRVAR(range_iterator___setstate____doc__,
+"__setstate__($self, state, /)\n"
+"--\n"
+"\n"
+"Set state information for unpickling.");
+
+#define RANGE_ITERATOR___SETSTATE___METHODDEF    \
+    {"__setstate__", (PyCFunction)range_iterator___setstate__, METH_O, range_iterator___setstate____doc__},
+
+static PyObject *
+range_iterator___setstate___impl(_PyRangeIterObject *r, PyObject *state);
+
+static PyObject *
+range_iterator___setstate__(PyObject *r, PyObject *state)
+{
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(r);
+    return_value = range_iterator___setstate___impl((_PyRangeIterObject *)r, state);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
+}
+
+PyDoc_STRVAR(longrange_iterator___length_hint____doc__,
+"__length_hint__($self, /)\n"
+"--\n"
+"\n"
+"Private method returning an estimate of len(list(it)).");
+
+#define LONGRANGE_ITERATOR___LENGTH_HINT___METHODDEF    \
+    {"__length_hint__", (PyCFunction)longrange_iterator___length_hint__, METH_NOARGS, longrange_iterator___length_hint____doc__},
+
+static PyObject *
+longrange_iterator___length_hint___impl(longrangeiterobject *r);
+
+static PyObject *
+longrange_iterator___length_hint__(PyObject *r, PyObject *Py_UNUSED(ignored))
+{
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(r);
+    return_value = longrange_iterator___length_hint___impl((longrangeiterobject *)r);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
+}
+
+PyDoc_STRVAR(longrange_iterator___reduce____doc__,
+"__reduce__($self, /)\n"
+"--\n"
+"\n"
+"Return state information for pickling.");
+
+#define LONGRANGE_ITERATOR___REDUCE___METHODDEF    \
+    {"__reduce__", (PyCFunction)longrange_iterator___reduce__, METH_NOARGS, longrange_iterator___reduce____doc__},
+
+static PyObject *
+longrange_iterator___reduce___impl(longrangeiterobject *r);
+
+static PyObject *
+longrange_iterator___reduce__(PyObject *r, PyObject *Py_UNUSED(ignored))
+{
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(r);
+    return_value = longrange_iterator___reduce___impl((longrangeiterobject *)r);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
+}
+
+PyDoc_STRVAR(longrange_iterator___setstate____doc__,
+"__setstate__($self, state, /)\n"
+"--\n"
+"\n"
+"Set state information for unpickling.");
+
+#define LONGRANGE_ITERATOR___SETSTATE___METHODDEF    \
+    {"__setstate__", (PyCFunction)longrange_iterator___setstate__, METH_O, longrange_iterator___setstate____doc__},
+
+static PyObject *
+longrange_iterator___setstate___impl(longrangeiterobject *r, PyObject *state);
+
+static PyObject *
+longrange_iterator___setstate__(PyObject *r, PyObject *state)
+{
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(r);
+    return_value = longrange_iterator___setstate___impl((longrangeiterobject *)r, state);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
+}
+/*[clinic end generated code: output=719c0e4c81fe0f4a input=a9049054013a1b77]*/
index e93346fb27703f6fe4ad11c488148a7882021b0e..55b7f108730728dafb5c304e41dc18ee7a788e9f 100644 (file)
@@ -9,6 +9,21 @@
 #include "pycore_range.h"
 #include "pycore_tuple.h"         // _PyTuple_ITEMS()
 
+typedef struct {
+    PyObject_HEAD
+    PyObject *start;
+    PyObject *step;
+    PyObject *len;
+} longrangeiterobject;
+
+/*[clinic input]
+class range_iterator "_PyRangeIterObject *" "&PyRangeIter_Type"
+class longrange_iterator "longrangeiterobject *" "&PyLongRangeIter_Type"
+[clinic start generated code]*/
+/*[clinic end generated code: output=da39a3ee5e6b4b0d input=c7d97a63d1cfa6b3]*/
+
+#include "clinic/rangeobject.c.h"
+
 
 /* Support objects whose length is > PY_SSIZE_T_MAX.
 
@@ -830,30 +845,46 @@ PyTypeObject PyRange_Type = {
 static PyObject *
 rangeiter_next(PyObject *op)
 {
+    PyObject *ret = NULL;
+    Py_BEGIN_CRITICAL_SECTION(op);
     _PyRangeIterObject *r = (_PyRangeIterObject*)op;
     if (r->len > 0) {
         long result = r->start;
         r->start = result + r->step;
         r->len--;
-        return PyLong_FromLong(result);
+        ret = PyLong_FromLong(result);
     }
-    return NULL;
+    Py_END_CRITICAL_SECTION();
+    return ret;
 }
 
+/*[clinic input]
+@critical_section
+range_iterator.__length_hint__
+    self as r: self(type="_PyRangeIterObject *")
+
+Private method returning an estimate of len(list(it)).
+[clinic start generated code]*/
+
 static PyObject *
-rangeiter_len(PyObject *op, PyObject *Py_UNUSED(ignored))
+range_iterator___length_hint___impl(_PyRangeIterObject *r)
+/*[clinic end generated code: output=9ba6f22b1fc23dcc input=e3eb311e99d76e43]*/
 {
-    _PyRangeIterObject *r = (_PyRangeIterObject*)op;
     return PyLong_FromLong(r->len);
 }
 
-PyDoc_STRVAR(length_hint_doc,
-             "Private method returning an estimate of len(list(it)).");
+/*[clinic input]
+@critical_section
+range_iterator.__reduce__
+    self as r: self(type="_PyRangeIterObject *")
+
+Return state information for pickling.
+[clinic start generated code]*/
 
 static PyObject *
-rangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
+range_iterator___reduce___impl(_PyRangeIterObject *r)
+/*[clinic end generated code: output=c44d53750c388415 input=75a25b7076dc2c54]*/
 {
-    _PyRangeIterObject *r = (_PyRangeIterObject*)op;
     PyObject *start=NULL, *stop=NULL, *step=NULL;
     PyObject *range;
 
@@ -881,10 +912,20 @@ err:
     return NULL;
 }
 
+/*[clinic input]
+@critical_section
+range_iterator.__setstate__
+    self as r: self(type="_PyRangeIterObject *")
+    state: object
+    /
+
+Set state information for unpickling.
+[clinic start generated code]*/
+
 static PyObject *
-rangeiter_setstate(PyObject *op, PyObject *state)
+range_iterator___setstate___impl(_PyRangeIterObject *r, PyObject *state)
+/*[clinic end generated code: output=464b3cbafc2e3562 input=c8c84fab2519d200]*/
 {
-    _PyRangeIterObject *r = (_PyRangeIterObject*)op;
     long index = PyLong_AsLong(state);
     if (index == -1 && PyErr_Occurred())
         return NULL;
@@ -904,13 +945,10 @@ rangeiter_dealloc(PyObject *self)
     _Py_FREELIST_FREE(range_iters, (_PyRangeIterObject *)self, PyObject_Free);
 }
 
-PyDoc_STRVAR(reduce_doc, "Return state information for pickling.");
-PyDoc_STRVAR(setstate_doc, "Set state information for unpickling.");
-
 static PyMethodDef rangeiter_methods[] = {
-    {"__length_hint__", rangeiter_len, METH_NOARGS, length_hint_doc},
-    {"__reduce__", rangeiter_reduce, METH_NOARGS, reduce_doc},
-    {"__setstate__", rangeiter_setstate, METH_O, setstate_doc},
+    RANGE_ITERATOR___LENGTH_HINT___METHODDEF
+    RANGE_ITERATOR___REDUCE___METHODDEF
+    RANGE_ITERATOR___SETSTATE___METHODDEF
     {NULL,              NULL}           /* sentinel */
 };
 
@@ -995,25 +1033,34 @@ fast_range_iter(long start, long stop, long step, long len)
     return (PyObject *)it;
 }
 
-typedef struct {
-    PyObject_HEAD
-    PyObject *start;
-    PyObject *step;
-    PyObject *len;
-} longrangeiterobject;
+/*[clinic input]
+@critical_section
+longrange_iterator.__length_hint__
+    self as r: self(type="longrangeiterobject *")
+
+Private method returning an estimate of len(list(it)).
+[clinic start generated code]*/
 
 static PyObject *
-longrangeiter_len(PyObject *op, PyObject *Py_UNUSED(ignored))
+longrange_iterator___length_hint___impl(longrangeiterobject *r)
+/*[clinic end generated code: output=e1bce24da7e8bfde input=ba94b050d940411e]*/
 {
-    longrangeiterobject *r = (longrangeiterobject*)op;
     Py_INCREF(r->len);
     return r->len;
 }
 
+/*[clinic input]
+@critical_section
+longrange_iterator.__reduce__
+    self as r: self(type="longrangeiterobject *")
+
+Return state information for pickling.
+[clinic start generated code]*/
+
 static PyObject *
-longrangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
+longrange_iterator___reduce___impl(longrangeiterobject *r)
+/*[clinic end generated code: output=0077f94ae2a4e99a input=2e8930e897ace086]*/
 {
-    longrangeiterobject *r = (longrangeiterobject*)op;
     PyObject *product, *stop=NULL;
     PyObject *range;
 
@@ -1039,15 +1086,25 @@ longrangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
                          range, Py_None);
 }
 
+/*[clinic input]
+@critical_section
+longrange_iterator.__setstate__
+    self as r: self(type="longrangeiterobject *")
+    state: object
+    /
+
+Set state information for unpickling.
+[clinic start generated code]*/
+
 static PyObject *
-longrangeiter_setstate(PyObject *op, PyObject *state)
+longrange_iterator___setstate___impl(longrangeiterobject *r, PyObject *state)
+/*[clinic end generated code: output=870787f0574f0da4 input=8b116de3018de824]*/
 {
     if (!PyLong_CheckExact(state)) {
         PyErr_Format(PyExc_TypeError, "state must be an int, not %T", state);
         return NULL;
     }
 
-    longrangeiterobject *r = (longrangeiterobject*)op;
     PyObject *zero = _PyLong_GetZero();  // borrowed reference
     int cmp;
 
@@ -1085,9 +1142,9 @@ longrangeiter_setstate(PyObject *op, PyObject *state)
 }
 
 static PyMethodDef longrangeiter_methods[] = {
-    {"__length_hint__", longrangeiter_len, METH_NOARGS, length_hint_doc},
-    {"__reduce__", longrangeiter_reduce, METH_NOARGS, reduce_doc},
-    {"__setstate__", longrangeiter_setstate, METH_O, setstate_doc},
+    LONGRANGE_ITERATOR___LENGTH_HINT___METHODDEF
+    LONGRANGE_ITERATOR___REDUCE___METHODDEF
+    LONGRANGE_ITERATOR___SETSTATE___METHODDEF
     {NULL,              NULL}           /* sentinel */
 };
 
@@ -1102,7 +1159,7 @@ longrangeiter_dealloc(PyObject *op)
 }
 
 static PyObject *
-longrangeiter_next(PyObject *op)
+longrangeiter_next_lock_held(PyObject *op)
 {
     longrangeiterobject *r = (longrangeiterobject*)op;
     if (PyObject_RichCompareBool(r->len, _PyLong_GetZero(), Py_GT) != 1)
@@ -1123,6 +1180,16 @@ longrangeiter_next(PyObject *op)
     return result;
 }
 
+static PyObject *
+longrangeiter_next(PyObject *op)
+{
+    PyObject *result;
+    Py_BEGIN_CRITICAL_SECTION(op);
+    result = longrangeiter_next_lock_held(op);
+    Py_END_CRITICAL_SECTION();
+    return result;
+}
+
 PyTypeObject PyLongRangeIter_Type = {
         PyVarObject_HEAD_INIT(&PyType_Type, 0)
         "longrange_iterator",                   /* tp_name */
index 46489f5cd9dbae9ea43f4550370a2fb7b2b25d6b..adc85d631db7c634df2f21f64a26a3361530118a 100644 (file)
@@ -20,15 +20,9 @@ race_top:_PyObject_TryGetInstanceAttribute
 race_top:PyUnstable_InterpreterFrame_GetLine
 race_top:write_thread_id
 
-# gh-129068: race on shared range iterators (test_free_threading.test_zip.ZipThreading.test_threading)
-race_top:rangeiter_next
-
 # https://gist.github.com/mpage/6962e8870606cfc960e159b407a0cb40
 thread:pthread_create
 
-# Range iteration is not thread-safe yet (issue #129068)
-race_top:rangeiter_next
-
 # List resizing happens through different paths ending in memcpy or memmove
 # (for efficiency), which will probably need to rewritten as explicit loops
 # of ptr-sized copies to be thread-safe. (Issue #129069)