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.
This commit is contained in:
Sam Gross 2025-12-18 13:11:51 -05:00 committed by GitHub
parent e22c49522b
commit f54d44d333
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 251 additions and 38 deletions

View file

@ -0,0 +1,2 @@
Make concurrent iteration over the same range iterator thread-safe in the
free threading build.

150
Objects/clinic/rangeobject.c.h generated Normal file
View file

@ -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]*/

View 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 @@ 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 */

View 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)