https://github.com/python/cpython/commit/f54d44d3331ef0bc19f1437f32ffde2e49c07c22
commit: f54d44d3331ef0bc19f1437f32ffde2e49c07c22
branch: main
author: Sam Gross <[email protected]>
committer: colesbury <[email protected]>
date: 2025-12-18T13:11:51-05:00
summary:
gh-129068: Make range iterators thread-safe (gh-142886)
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.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-10-49-03.gh-issue-129068.GlYnrO.rst
A Objects/clinic/rangeobject.c.h
M Objects/rangeobject.c
M 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
index 00000000000000..16b19524cd4057
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-17-10-49-03.gh-issue-129068.GlYnrO.rst
@@ -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
index 00000000000000..d9142eddde4b17
--- /dev/null
+++ b/Objects/clinic/rangeobject.c.h
@@ -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]*/
diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c
index e93346fb27703f..55b7f108730728 100644
--- a/Objects/rangeobject.c
+++ b/Objects/rangeobject.c
@@ -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 @@ rangeiter_reduce(PyObject *op, PyObject
*Py_UNUSED(ignored))
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 */
diff --git a/Tools/tsan/suppressions_free_threading.txt
b/Tools/tsan/suppressions_free_threading.txt
index 46489f5cd9dbae..adc85d631db7c6 100644
--- a/Tools/tsan/suppressions_free_threading.txt
+++ b/Tools/tsan/suppressions_free_threading.txt
@@ -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)
_______________________________________________
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]