https://github.com/python/cpython/commit/1fb7e2aeb7e4312b7f20f0d5f39ddd00d7762004 commit: 1fb7e2aeb7e4312b7f20f0d5f39ddd00d7762004 branch: main author: Pieter Eendebak <pieter.eende...@gmail.com> committer: ambv <luk...@langa.pl> date: 2025-03-12T12:39:52+01:00 summary:
gh-120608: Make reversed iterator work with free-threading (#120971) Co-authored-by: Sam Gross <colesb...@gmail.com> Co-authored-by: Kumar Aditya <kumaradi...@python.org> Co-authored-by: Ćukasz Langa <luk...@langa.pl> files: A Lib/test/test_free_threading/test_reversed.py A Misc/NEWS.d/next/Core_and_Builtins/2024-06-24-20-08-55.gh-issue-120608.d75n8U.rst M Lib/test/test_str.py M Objects/enumobject.c diff --git a/Lib/test/test_free_threading/test_reversed.py b/Lib/test/test_free_threading/test_reversed.py new file mode 100644 index 00000000000000..dd6016489c88ec --- /dev/null +++ b/Lib/test/test_free_threading/test_reversed.py @@ -0,0 +1,39 @@ +import unittest +from threading import Barrier, Thread +from test.support import threading_helper + +threading_helper.requires_working_threading(module=True) + +class TestReversed(unittest.TestCase): + + @threading_helper.reap_threads + def test_reversed(self): + # Iterating over the iterator with multiple threads should not + # emit TSAN warnings + number_of_iterations = 10 + number_of_threads = 10 + size = 1_000 + + barrier = Barrier(number_of_threads) + def work(r): + barrier.wait() + while True: + try: + l = r.__length_hint__() + next(r) + except StopIteration: + break + assert 0 <= l <= size + x = tuple(range(size)) + + for _ in range(number_of_iterations): + r = reversed(x) + worker_threads = [] + for _ in range(number_of_threads): + worker_threads.append(Thread(target=work, args=[r])) + with threading_helper.start_threads(worker_threads): + pass + barrier.reset() + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 2694f5d45c7ebf..d6a7bd0da59910 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -2605,7 +2605,8 @@ def test_compare(self): def test_free_after_iterating(self): support.check_free_after_iterating(self, iter, str) - support.check_free_after_iterating(self, reversed, str) + if not support.Py_GIL_DISABLED: + support.check_free_after_iterating(self, reversed, str) def test_check_encoding_errors(self): # bpo-37388: str(bytes) and str.decode() must check encoding and errors diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-06-24-20-08-55.gh-issue-120608.d75n8U.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-06-24-20-08-55.gh-issue-120608.d75n8U.rst new file mode 100644 index 00000000000000..31d1dfd54d7933 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-06-24-20-08-55.gh-issue-120608.d75n8U.rst @@ -0,0 +1,4 @@ +Adapt :func:`reversed` for use in the free-theading build. +The :func:`reversed` is still not thread-safe in the sense that concurrent +iterations may see the same object, but they will not corrupt the interpreter +state. diff --git a/Objects/enumobject.c b/Objects/enumobject.c index eb8952675269a2..17510b29ee70b5 100644 --- a/Objects/enumobject.c +++ b/Objects/enumobject.c @@ -439,20 +439,22 @@ reversed_next(PyObject *op) { reversedobject *ro = _reversedobject_CAST(op); PyObject *item; - Py_ssize_t index = ro->index; + Py_ssize_t index = FT_ATOMIC_LOAD_SSIZE_RELAXED(ro->index); if (index >= 0) { item = PySequence_GetItem(ro->seq, index); if (item != NULL) { - ro->index--; + FT_ATOMIC_STORE_SSIZE_RELAXED(ro->index, index - 1); return item; } if (PyErr_ExceptionMatches(PyExc_IndexError) || PyErr_ExceptionMatches(PyExc_StopIteration)) PyErr_Clear(); } - ro->index = -1; + FT_ATOMIC_STORE_SSIZE_RELAXED(ro->index, -1); +#ifndef Py_GIL_DISABLED Py_CLEAR(ro->seq); +#endif return NULL; } @@ -461,13 +463,15 @@ reversed_len(PyObject *op, PyObject *Py_UNUSED(ignored)) { reversedobject *ro = _reversedobject_CAST(op); Py_ssize_t position, seqsize; + Py_ssize_t index = FT_ATOMIC_LOAD_SSIZE_RELAXED(ro->index); - if (ro->seq == NULL) + if (index == -1) return PyLong_FromLong(0); + assert(ro->seq != NULL); seqsize = PySequence_Size(ro->seq); if (seqsize == -1) return NULL; - position = ro->index + 1; + position = index + 1; return PyLong_FromSsize_t((seqsize < position) ? 0 : position); } @@ -477,10 +481,13 @@ static PyObject * reversed_reduce(PyObject *op, PyObject *Py_UNUSED(ignored)) { reversedobject *ro = _reversedobject_CAST(op); - if (ro->seq) + Py_ssize_t index = FT_ATOMIC_LOAD_SSIZE_RELAXED(ro->index); + if (index != -1) { return Py_BuildValue("O(O)n", Py_TYPE(ro), ro->seq, ro->index); - else + } + else { return Py_BuildValue("O(())", Py_TYPE(ro)); + } } static PyObject * @@ -490,7 +497,11 @@ reversed_setstate(PyObject *op, PyObject *state) Py_ssize_t index = PyLong_AsSsize_t(state); if (index == -1 && PyErr_Occurred()) return NULL; - if (ro->seq != 0) { + Py_ssize_t ro_index = FT_ATOMIC_LOAD_SSIZE_RELAXED(ro->index); + // if the iterator is exhausted we do not set the state + // this is for backwards compatibility reasons. in practice this situation + // will not occur, see gh-120971 + if (ro_index != -1) { Py_ssize_t n = PySequence_Size(ro->seq); if (n < 0) return NULL; @@ -498,7 +509,7 @@ reversed_setstate(PyObject *op, PyObject *state) index = -1; else if (index > n-1) index = n-1; - ro->index = index; + FT_ATOMIC_STORE_SSIZE_RELAXED(ro->index, index); } Py_RETURN_NONE; } _______________________________________________ Python-checkins mailing list -- python-checkins@python.org To unsubscribe send an email to python-checkins-le...@python.org https://mail.python.org/mailman3/lists/python-checkins.python.org/ Member address: arch...@mail-archive.com