Skip to content

Commit f54d44d

Browse files
authored
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.
1 parent e22c495 commit f54d44d

File tree

4 files changed

+251
-38
lines changed

4 files changed

+251
-38
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make concurrent iteration over the same range iterator thread-safe in the
2+
free threading build.

Objects/clinic/rangeobject.c.h

Lines changed: 150 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Objects/rangeobject.c

Lines changed: 99 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@
99
#include "pycore_range.h"
1010
#include "pycore_tuple.h" // _PyTuple_ITEMS()
1111

12+
typedef struct {
13+
PyObject_HEAD
14+
PyObject *start;
15+
PyObject *step;
16+
PyObject *len;
17+
} longrangeiterobject;
18+
19+
/*[clinic input]
20+
class range_iterator "_PyRangeIterObject *" "&PyRangeIter_Type"
21+
class longrange_iterator "longrangeiterobject *" "&PyLongRangeIter_Type"
22+
[clinic start generated code]*/
23+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=c7d97a63d1cfa6b3]*/
24+
25+
#include "clinic/rangeobject.c.h"
26+
1227

1328
/* Support objects whose length is > PY_SSIZE_T_MAX.
1429
@@ -830,30 +845,46 @@ PyTypeObject PyRange_Type = {
830845
static PyObject *
831846
rangeiter_next(PyObject *op)
832847
{
848+
PyObject *ret = NULL;
849+
Py_BEGIN_CRITICAL_SECTION(op);
833850
_PyRangeIterObject *r = (_PyRangeIterObject*)op;
834851
if (r->len > 0) {
835852
long result = r->start;
836853
r->start = result + r->step;
837854
r->len--;
838-
return PyLong_FromLong(result);
855+
ret = PyLong_FromLong(result);
839856
}
840-
return NULL;
857+
Py_END_CRITICAL_SECTION();
858+
return ret;
841859
}
842860

861+
/*[clinic input]
862+
@critical_section
863+
range_iterator.__length_hint__
864+
self as r: self(type="_PyRangeIterObject *")
865+
866+
Private method returning an estimate of len(list(it)).
867+
[clinic start generated code]*/
868+
843869
static PyObject *
844-
rangeiter_len(PyObject *op, PyObject *Py_UNUSED(ignored))
870+
range_iterator___length_hint___impl(_PyRangeIterObject *r)
871+
/*[clinic end generated code: output=9ba6f22b1fc23dcc input=e3eb311e99d76e43]*/
845872
{
846-
_PyRangeIterObject *r = (_PyRangeIterObject*)op;
847873
return PyLong_FromLong(r->len);
848874
}
849875

850-
PyDoc_STRVAR(length_hint_doc,
851-
"Private method returning an estimate of len(list(it)).");
876+
/*[clinic input]
877+
@critical_section
878+
range_iterator.__reduce__
879+
self as r: self(type="_PyRangeIterObject *")
880+
881+
Return state information for pickling.
882+
[clinic start generated code]*/
852883

853884
static PyObject *
854-
rangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
885+
range_iterator___reduce___impl(_PyRangeIterObject *r)
886+
/*[clinic end generated code: output=c44d53750c388415 input=75a25b7076dc2c54]*/
855887
{
856-
_PyRangeIterObject *r = (_PyRangeIterObject*)op;
857888
PyObject *start=NULL, *stop=NULL, *step=NULL;
858889
PyObject *range;
859890

@@ -881,10 +912,20 @@ rangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
881912
return NULL;
882913
}
883914

915+
/*[clinic input]
916+
@critical_section
917+
range_iterator.__setstate__
918+
self as r: self(type="_PyRangeIterObject *")
919+
state: object
920+
/
921+
922+
Set state information for unpickling.
923+
[clinic start generated code]*/
924+
884925
static PyObject *
885-
rangeiter_setstate(PyObject *op, PyObject *state)
926+
range_iterator___setstate___impl(_PyRangeIterObject *r, PyObject *state)
927+
/*[clinic end generated code: output=464b3cbafc2e3562 input=c8c84fab2519d200]*/
886928
{
887-
_PyRangeIterObject *r = (_PyRangeIterObject*)op;
888929
long index = PyLong_AsLong(state);
889930
if (index == -1 && PyErr_Occurred())
890931
return NULL;
@@ -904,13 +945,10 @@ rangeiter_dealloc(PyObject *self)
904945
_Py_FREELIST_FREE(range_iters, (_PyRangeIterObject *)self, PyObject_Free);
905946
}
906947

907-
PyDoc_STRVAR(reduce_doc, "Return state information for pickling.");
908-
PyDoc_STRVAR(setstate_doc, "Set state information for unpickling.");
909-
910948
static PyMethodDef rangeiter_methods[] = {
911-
{"__length_hint__", rangeiter_len, METH_NOARGS, length_hint_doc},
912-
{"__reduce__", rangeiter_reduce, METH_NOARGS, reduce_doc},
913-
{"__setstate__", rangeiter_setstate, METH_O, setstate_doc},
949+
RANGE_ITERATOR___LENGTH_HINT___METHODDEF
950+
RANGE_ITERATOR___REDUCE___METHODDEF
951+
RANGE_ITERATOR___SETSTATE___METHODDEF
914952
{NULL, NULL} /* sentinel */
915953
};
916954

@@ -995,25 +1033,34 @@ fast_range_iter(long start, long stop, long step, long len)
9951033
return (PyObject *)it;
9961034
}
9971035

998-
typedef struct {
999-
PyObject_HEAD
1000-
PyObject *start;
1001-
PyObject *step;
1002-
PyObject *len;
1003-
} longrangeiterobject;
1036+
/*[clinic input]
1037+
@critical_section
1038+
longrange_iterator.__length_hint__
1039+
self as r: self(type="longrangeiterobject *")
1040+
1041+
Private method returning an estimate of len(list(it)).
1042+
[clinic start generated code]*/
10041043

10051044
static PyObject *
1006-
longrangeiter_len(PyObject *op, PyObject *Py_UNUSED(ignored))
1045+
longrange_iterator___length_hint___impl(longrangeiterobject *r)
1046+
/*[clinic end generated code: output=e1bce24da7e8bfde input=ba94b050d940411e]*/
10071047
{
1008-
longrangeiterobject *r = (longrangeiterobject*)op;
10091048
Py_INCREF(r->len);
10101049
return r->len;
10111050
}
10121051

1052+
/*[clinic input]
1053+
@critical_section
1054+
longrange_iterator.__reduce__
1055+
self as r: self(type="longrangeiterobject *")
1056+
1057+
Return state information for pickling.
1058+
[clinic start generated code]*/
1059+
10131060
static PyObject *
1014-
longrangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
1061+
longrange_iterator___reduce___impl(longrangeiterobject *r)
1062+
/*[clinic end generated code: output=0077f94ae2a4e99a input=2e8930e897ace086]*/
10151063
{
1016-
longrangeiterobject *r = (longrangeiterobject*)op;
10171064
PyObject *product, *stop=NULL;
10181065
PyObject *range;
10191066

@@ -1039,15 +1086,25 @@ longrangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
10391086
range, Py_None);
10401087
}
10411088

1089+
/*[clinic input]
1090+
@critical_section
1091+
longrange_iterator.__setstate__
1092+
self as r: self(type="longrangeiterobject *")
1093+
state: object
1094+
/
1095+
1096+
Set state information for unpickling.
1097+
[clinic start generated code]*/
1098+
10421099
static PyObject *
1043-
longrangeiter_setstate(PyObject *op, PyObject *state)
1100+
longrange_iterator___setstate___impl(longrangeiterobject *r, PyObject *state)
1101+
/*[clinic end generated code: output=870787f0574f0da4 input=8b116de3018de824]*/
10441102
{
10451103
if (!PyLong_CheckExact(state)) {
10461104
PyErr_Format(PyExc_TypeError, "state must be an int, not %T", state);
10471105
return NULL;
10481106
}
10491107

1050-
longrangeiterobject *r = (longrangeiterobject*)op;
10511108
PyObject *zero = _PyLong_GetZero(); // borrowed reference
10521109
int cmp;
10531110

@@ -1085,9 +1142,9 @@ longrangeiter_setstate(PyObject *op, PyObject *state)
10851142
}
10861143

10871144
static PyMethodDef longrangeiter_methods[] = {
1088-
{"__length_hint__", longrangeiter_len, METH_NOARGS, length_hint_doc},
1089-
{"__reduce__", longrangeiter_reduce, METH_NOARGS, reduce_doc},
1090-
{"__setstate__", longrangeiter_setstate, METH_O, setstate_doc},
1145+
LONGRANGE_ITERATOR___LENGTH_HINT___METHODDEF
1146+
LONGRANGE_ITERATOR___REDUCE___METHODDEF
1147+
LONGRANGE_ITERATOR___SETSTATE___METHODDEF
10911148
{NULL, NULL} /* sentinel */
10921149
};
10931150

@@ -1102,7 +1159,7 @@ longrangeiter_dealloc(PyObject *op)
11021159
}
11031160

11041161
static PyObject *
1105-
longrangeiter_next(PyObject *op)
1162+
longrangeiter_next_lock_held(PyObject *op)
11061163
{
11071164
longrangeiterobject *r = (longrangeiterobject*)op;
11081165
if (PyObject_RichCompareBool(r->len, _PyLong_GetZero(), Py_GT) != 1)
@@ -1123,6 +1180,16 @@ longrangeiter_next(PyObject *op)
11231180
return result;
11241181
}
11251182

1183+
static PyObject *
1184+
longrangeiter_next(PyObject *op)
1185+
{
1186+
PyObject *result;
1187+
Py_BEGIN_CRITICAL_SECTION(op);
1188+
result = longrangeiter_next_lock_held(op);
1189+
Py_END_CRITICAL_SECTION();
1190+
return result;
1191+
}
1192+
11261193
PyTypeObject PyLongRangeIter_Type = {
11271194
PyVarObject_HEAD_INIT(&PyType_Type, 0)
11281195
"longrange_iterator", /* tp_name */

Tools/tsan/suppressions_free_threading.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,9 @@ race_top:_PyObject_TryGetInstanceAttribute
2020
race_top:PyUnstable_InterpreterFrame_GetLine
2121
race_top:write_thread_id
2222

23-
# gh-129068: race on shared range iterators (test_free_threading.test_zip.ZipThreading.test_threading)
24-
race_top:rangeiter_next
25-
2623
# https://gist.github.com/mpage/6962e8870606cfc960e159b407a0cb40
2724
thread:pthread_create
2825

29-
# Range iteration is not thread-safe yet (issue #129068)
30-
race_top:rangeiter_next
31-
3226
# List resizing happens through different paths ending in memcpy or memmove
3327
# (for efficiency), which will probably need to rewritten as explicit loops
3428
# of ptr-sized copies to be thread-safe. (Issue #129069)

0 commit comments

Comments
 (0)