gh-120321: fix thread safety of concurrently iterating over async generators (#155025)

kumaraditya303 <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/61818b6087e59de6287ac71bd734a0420d6e8ee3
commit: 61818b6087e59de6287ac71bd734a0420d6e8ee3
branch: main
author: Kumar Aditya <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-08-19T08:55:00+05:30
summary:

gh-120321: fix thread safety of concurrently iterating over async generators (#155025)

files:
A Lib/test/test_free_threading/test_async_generators.py
A Misc/NEWS.d/next/Core_and_Builtins/2026-08-01-18-04-11.gh-issue-120321.k3XvQb.rst
M Include/internal/pycore_interpframe_structs.h
M Lib/test/test_asyncgen.py
M Objects/genobject.c

diff --git a/Include/internal/pycore_interpframe_structs.h b/Include/internal/pycore_interpframe_structs.h
index 4d267e35504b94d..560fb66a97fc89d 100644
--- a/Include/internal/pycore_interpframe_structs.h
+++ b/Include/internal/pycore_interpframe_structs.h
@@ -66,9 +66,9 @@ struct _PyInterpreterFrame {
     PyObject *prefix##_qualname;                                            \
     _PyErr_StackItem prefix##_exc_state;                                    \
     PyObject *prefix##_origin_or_finalizer;                                 \
-    char prefix##_hooks_inited;                                             \
-    char prefix##_closed;                                                   \
-    char prefix##_running_async;                                            \
+    int8_t prefix##_hooks_inited;                                           \
+    int8_t prefix##_closed;                                                 \
+    int8_t prefix##_running_async;                                          \
     /* The frame */                                                         \
     int8_t prefix##_frame_state;                                            \
     _PyInterpreterFrame prefix##_iframe;                                    \
diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py
index e9eecbc83415518..70a285dd91f385f 100644
--- a/Lib/test/test_asyncgen.py
+++ b/Lib/test/test_asyncgen.py
@@ -619,6 +619,33 @@ async def agenfn():
         with self.assertRaisesRegex(RuntimeError, "coroutine ignored GeneratorExit"):
             gen.close()
 
+    def test_async_gen_athrow_send_non_none(self):
+        # gh-120321: sending a non-None value to a just-started athrow()
+        # awaitable must not claim the generator, so the generator stays
+        # usable and the awaitable can still be awaited afterwards.
+        class MyExc(Exception):
+            pass
+
+        async def agenfn():
+            try:
+                yield 1
+            except MyExc:
+                yield 2
+
+        agen = agenfn()
+        with self.assertRaises(StopIteration):
+            agen.asend(None).send(None)
+
+        gen = agen.athrow(MyExc)
+        with self.assertRaisesRegex(RuntimeError, "non-None value"):
+            gen.send(42)
+        self.assertFalse(agen.ag_running)
+
+        # The awaitable is still in its initial state and works normally.
+        with self.assertRaises(StopIteration) as cm:
+            gen.send(None)
+        self.assertEqual(cm.exception.value, 2)
+
 
 class AsyncGenAsyncioTest(unittest.TestCase):
 
@@ -1950,6 +1977,41 @@ class MyException(Exception):
         ):
             nxt.throw(MyException)
 
+    def test_async_gen_send_same_athrow_coro_after_completion(self):
+        # gh-120321: an athrow() awaitable that needs more than one send()
+        # to complete must be closed on completion; sending to it again
+        # must raise instead of resuming the generator.
+        class YieldOnce:
+            def __await__(self):
+                yield
+
+        async def async_iterate():
+            try:
+                yield 1
+            except ValueError:
+                await YieldOnce()
+            yield 2
+
+        it = async_iterate()
+        with self.assertRaises(StopIteration):
+            it.__anext__().send(None)
+
+        nxt = it.athrow(ValueError)
+        # The exception handler suspends before the operation completes.
+        nxt.send(None)
+        with self.assertRaises(StopIteration) as cm:
+            nxt.send(None)
+        self.assertEqual(cm.exception.value, 2)
+
+        with self.assertRaisesRegex(
+            RuntimeError,
+            r"cannot reuse already awaited aclose\(\)/athrow\(\)"
+        ):
+            nxt.send(None)
+
+        with self.assertRaises(StopIteration):
+            it.aclose().send(None)
+
     def test_async_gen_aclose_twice_with_different_coros(self):
         # Regression test for https://bugs.python.org/issue39606
         async def async_iterate():
diff --git a/Lib/test/test_free_threading/test_async_generators.py b/Lib/test/test_free_threading/test_async_generators.py
new file mode 100644
index 000000000000000..7987ff6ed2fafec
--- /dev/null
+++ b/Lib/test/test_free_threading/test_async_generators.py
@@ -0,0 +1,248 @@
+import sys
+import unittest
+
+from test.support import threading_helper
+
+threading_helper.requires_working_threading(module=True)
+
+
+class TestFTAsyncGenerators(unittest.TestCase):
+    NUM_THREADS = 4
+
+    def test_concurrent_anext(self):
+        # Each yielded value must be delivered to exactly one thread.
+        async def agen():
+            for i in range(100):
+                yield i
+
+        ag = agen()
+        values = []
+
+        def drive():
+            while True:
+                try:
+                    ag.asend(None).send(None)
+                except StopIteration as e:
+                    values.append(e.value)
+                except StopAsyncIteration:
+                    break
+                except RuntimeError:
+                    # Another thread is currently driving the generator.
+                    continue
+
+        threading_helper.run_concurrently(drive, self.NUM_THREADS)
+        self.assertEqual(sorted(values), list(range(100)))
+
+    def test_concurrent_athrow(self):
+        # Each thrown exception must be delivered to the generator
+        # exactly once.
+        received = []
+
+        async def agen():
+            while True:
+                try:
+                    yield 1
+                except ValueError:
+                    received.append(1)
+
+        ag = agen()
+        with self.assertRaises(StopIteration):
+            ag.asend(None).send(None)  # advance to the first yield
+
+        delivered = []
+
+        def worker():
+            for _ in range(50):
+                try:
+                    ag.athrow(ValueError).send(None)
+                except StopIteration as e:
+                    # The generator received the exception and yielded again.
+                    delivered.append(e.value)
+                except RuntimeError:
+                    # Another thread is currently driving the generator.
+                    pass
+
+        threading_helper.run_concurrently(worker, self.NUM_THREADS)
+        self.assertEqual(len(received), len(delivered))
+
+    def test_concurrent_aclose(self):
+        # The generator must be cleaned up exactly once.
+        cleanups = []
+
+        async def agen():
+            try:
+                while True:
+                    yield 1
+            finally:
+                cleanups.append(1)
+
+        ag = agen()
+        with self.assertRaises(StopIteration):
+            ag.asend(None).send(None)  # advance to the first yield
+
+        def worker():
+            try:
+                ag.aclose().send(None)
+            except StopIteration:
+                # aclose() completed.
+                pass
+            except StopAsyncIteration:
+                # The generator was already closed.
+                pass
+            except RuntimeError:
+                # Another thread is currently driving the generator.
+                pass
+
+        threading_helper.run_concurrently(worker, self.NUM_THREADS)
+        self.assertEqual(cleanups, [1])
+        self.assertRaises(StopAsyncIteration, ag.asend(None).send, None)
+
+    def test_concurrent_shared_asend(self):
+        # Multiple threads racing on a single asend awaitable: the value
+        # must be delivered exactly once.
+        async def agen():
+            yield 1
+
+        ag = agen()
+        aw = ag.asend(None)
+        results = []
+
+        def worker():
+            try:
+                aw.send(None)
+            except StopIteration as e:
+                results.append(e.value)
+            except (RuntimeError, ValueError, StopAsyncIteration):
+                pass
+
+        threading_helper.run_concurrently(worker, self.NUM_THREADS)
+        self.assertEqual(results, [1])
+        # The awaitable is closed after the operation completed.
+        self.assertRaises(RuntimeError, aw.send, None)
+
+    def test_concurrent_shared_athrow(self):
+        # Multiple threads racing on a single athrow awaitable.
+        async def agen():
+            while True:
+                yield 1
+
+        ag = agen()
+        with self.assertRaises(StopIteration):
+            ag.asend(None).send(None)  # advance to the first yield
+        aw = ag.athrow(ValueError)
+
+        def worker():
+            try:
+                aw.send(None)
+            except (RuntimeError, ValueError,
+                    StopIteration, StopAsyncIteration):
+                pass
+
+        threading_helper.run_concurrently(worker, self.NUM_THREADS)
+        self.assertRaises(StopAsyncIteration, ag.asend(None).send, None)
+        # The awaitable is closed after the operation completed.
+        self.assertRaises(RuntimeError, aw.send, None)
+
+    def test_concurrent_shared_aclose(self):
+        # Multiple threads racing on a single aclose awaitable: the
+        # generator must be cleaned up exactly once.
+        cleanups = []
+
+        async def agen():
+            try:
+                while True:
+                    yield 1
+            finally:
+                cleanups.append(1)
+
+        ag = agen()
+        with self.assertRaises(StopIteration):
+            ag.asend(None).send(None)  # advance to the first yield
+        aw = ag.aclose()
+
+        def worker():
+            try:
+                aw.send(None)
+            except (RuntimeError, ValueError,
+                    StopIteration, StopAsyncIteration):
+                pass
+
+        threading_helper.run_concurrently(worker, self.NUM_THREADS)
+        self.assertEqual(cleanups, [1])
+        self.assertRaises(StopAsyncIteration, ag.asend(None).send, None)
+        # The awaitable is closed after the operation completed.
+        self.assertRaises(RuntimeError, aw.send, None)
+
+    def test_concurrent_anext_athrow(self):
+        async def agen():
+            while True:
+                try:
+                    yield 1
+                except ValueError:
+                    pass
+
+        ag = agen()
+
+        def worker():
+            for i in range(100):
+                try:
+                    if i % 2:
+                        ag.asend(None).send(None)
+                    else:
+                        ag.athrow(ValueError).send(None)
+                except (RuntimeError, ValueError,
+                        StopIteration, StopAsyncIteration):
+                    pass
+
+        threading_helper.run_concurrently(worker, self.NUM_THREADS)
+
+    def test_concurrent_anext_aclose(self):
+        async def agen():
+            for i in range(100):
+                yield i
+
+        ag = agen()
+
+        def anext_worker():
+            for _ in range(100):
+                try:
+                    ag.asend(None).send(None)
+                except (RuntimeError, ValueError,
+                        StopIteration, StopAsyncIteration):
+                    pass
+
+        def aclose_worker():
+            for _ in range(100):
+                try:
+                    ag.aclose().send(None)
+                except (RuntimeError, ValueError,
+                        StopIteration, StopAsyncIteration):
+                    pass
+
+        threading_helper.run_concurrently(
+            [anext_worker, aclose_worker, anext_worker, aclose_worker])
+
+    def test_firstiter_hook_called_once(self):
+        # Racing the first iteration must invoke the firstiter hook
+        # exactly once.
+        async def agen():
+            yield 1
+
+        ag = agen()
+        calls = []
+
+        def worker():
+            # Async generator hooks are per-thread state.
+            sys.set_asyncgen_hooks(firstiter=calls.append)
+            try:
+                ag.asend(None).send(None)
+            except (RuntimeError, ValueError,
+                    StopIteration, StopAsyncIteration):
+                pass
+
+        threading_helper.run_concurrently(worker, self.NUM_THREADS)
+        self.assertEqual(calls, [ag])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-01-18-04-11.gh-issue-120321.k3XvQb.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-01-18-04-11.gh-issue-120321.k3XvQb.rst
new file mode 100644
index 000000000000000..e87dcb6f689985c
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-01-18-04-11.gh-issue-120321.k3XvQb.rst
@@ -0,0 +1,5 @@
+Fix thread safety of :term:`asynchronous generators <asynchronous
+generator>` when iterated, closed or thrown into concurrently from multiple
+threads on the :term:`free threading` build. Also make an ``athrow()``
+awaitable single-use: reusing it after completion now raises
+:exc:`RuntimeError` instead of resuming the generator again.
diff --git a/Objects/genobject.c b/Objects/genobject.c
index 3cdc06733363d3e..6529a66fc35a6b4 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -1604,6 +1604,44 @@ typedef enum {
     AWAITABLE_STATE_CLOSED, /* closed */
 } AwaitableState;
 
+#ifdef Py_GIL_DISABLED
+static bool
+async_gen_try_set_state(int8_t *state, int8_t *expected, int8_t new_state)
+{
+    return _Py_atomic_compare_exchange_int8(state, expected, new_state);
+}
+
+# define _Py_ASYNC_GEN_TRY_SET_STATE(state, expected, new_state) \
+    async_gen_try_set_state(&(state), &(expected), (new_state))
+#else
+# define _Py_ASYNC_GEN_TRY_SET_STATE(state, expected, new_state) \
+    ((state) = (new_state), true)
+#endif
+
+// Try to transition the async generator to the running state.
+// Returns false if it is already running.
+//
+// There are two ways to concurrently iterate an async generator: by
+// sharing a single asend()/athrow() object across threads, or with
+// multiple asend()/athrow() objects sending to the same generator.
+// The CAS on ags_state/agt_state handles the first case; the CAS on
+// ag_running_async here handles the second.
+static bool
+async_gen_try_claim_running(PyAsyncGenObject *agen)
+{
+#ifdef Py_GIL_DISABLED
+    int8_t expected = 0;
+    return _Py_atomic_compare_exchange_int8(&agen->ag_running_async,
+                                            &expected, 1);
+#else
+    if (agen->ag_running_async) {
+        return false;
+    }
+    agen->ag_running_async = 1;
+    return true;
+#endif
+}
+
 
 typedef struct PyAsyncGenASend {
     PyObject_HEAD
@@ -1613,7 +1651,7 @@ typedef struct PyAsyncGenASend {
        (equivalent of "asend(None)") */
     PyObject *ags_sendval;
 
-    AwaitableState ags_state;
+    int8_t ags_state;
 } PyAsyncGenASend;
 
 #define _PyAsyncGenASend_CAST(op) \
@@ -1630,7 +1668,7 @@ typedef struct PyAsyncGenAThrow {
     PyObject *agt_tb;
     PyObject *agt_val;
 
-    AwaitableState agt_state;
+    int8_t agt_state;
 } PyAsyncGenAThrow;
 
 
@@ -1672,11 +1710,17 @@ async_gen_init_hooks(PyAsyncGenObject *o)
     PyObject *finalizer;
     PyObject *firstiter;
 
+#ifdef Py_GIL_DISABLED
+    if (_Py_atomic_exchange_int8(&o->ag_hooks_inited, 1)) {
+        return 0;
+    }
+#else
     if (o->ag_hooks_inited) {
         return 0;
     }
 
     o->ag_hooks_inited = 1;
+#endif
 
     tstate = _PyThreadState_GET();
 
@@ -1919,10 +1963,9 @@ async_gen_unwrap_value(PyAsyncGenObject *gen, PyObject *result)
         if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)
             || PyErr_ExceptionMatches(PyExc_GeneratorExit)
         ) {
-            gen->ag_closed = 1;
+            FT_ATOMIC_STORE_INT8_RELAXED(gen->ag_closed, 1);
         }
 
-        gen->ag_running_async = 0;
         return NULL;
     }
 
@@ -1930,7 +1973,6 @@ async_gen_unwrap_value(PyAsyncGenObject *gen, PyObject *result)
         /* async yield */
         _PyGen_SetStopIterationValue(((_PyAsyncGenWrappedValue*)result)->agw_val);
         Py_DECREF(result);
-        gen->ag_running_async = 0;
         return NULL;
     }
 
@@ -1974,34 +2016,45 @@ static PyObject *
 async_gen_asend_send(PyObject *self, PyObject *arg)
 {
     PyAsyncGenASend *o = _PyAsyncGenASend_CAST(self);
-    if (o->ags_state == AWAITABLE_STATE_CLOSED) {
-        PyErr_SetString(
-            PyExc_RuntimeError,
-            "cannot reuse already awaited __anext__()/asend()");
-        return NULL;
-    }
 
-    if (o->ags_state == AWAITABLE_STATE_INIT) {
-        if (o->ags_gen->ag_running_async) {
-            o->ags_state = AWAITABLE_STATE_CLOSED;
+    int8_t state = FT_ATOMIC_LOAD_INT8_RELAXED(o->ags_state);
+    do {
+        if (state == AWAITABLE_STATE_CLOSED) {
             PyErr_SetString(
                 PyExc_RuntimeError,
-                "anext(): asynchronous generator is already running");
+                "cannot reuse already awaited __anext__()/asend()");
             return NULL;
         }
-
-        if (arg == NULL || arg == Py_None) {
-            arg = o->ags_sendval;
+        if (state == AWAITABLE_STATE_ITER) {
+            goto do_send;
         }
-        o->ags_state = AWAITABLE_STATE_ITER;
+        assert(state == AWAITABLE_STATE_INIT);
+    } while (!_Py_ASYNC_GEN_TRY_SET_STATE(o->ags_state, state,
+                                          AWAITABLE_STATE_ITER));
+
+    // The transition above only guards this object, the generator may
+    // still be running through another asend()/athrow() object so
+    // try to claim it before running.
+    if (!async_gen_try_claim_running(o->ags_gen)) {
+        FT_ATOMIC_STORE_INT8_RELAXED(o->ags_state, AWAITABLE_STATE_CLOSED);
+        PyErr_SetString(
+            PyExc_RuntimeError,
+            "anext(): asynchronous generator is already running");
+        return NULL;
     }
 
-    o->ags_gen->ag_running_async = 1;
-    PyObject *result = gen_send((PyObject*)o->ags_gen, arg);
+    if (arg == NULL || arg == Py_None) {
+        arg = o->ags_sendval;
+    }
+
+    PyObject *result;
+do_send:
+    result = gen_send((PyObject*)o->ags_gen, arg);
     result = async_gen_unwrap_value(o->ags_gen, result);
 
     if (result == NULL) {
-        o->ags_state = AWAITABLE_STATE_CLOSED;
+        FT_ATOMIC_STORE_INT8_RELAXED(o->ags_state, AWAITABLE_STATE_CLOSED);
+        FT_ATOMIC_STORE_INT8_RELEASE(o->ags_gen->ag_running_async, 0);
     }
 
     return result;
@@ -2033,32 +2086,40 @@ async_gen_asend_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
 {
     PyAsyncGenASend *o = _PyAsyncGenASend_CAST(self);
 
-    if (o->ags_state == AWAITABLE_STATE_CLOSED) {
-        PyErr_SetString(
-            PyExc_RuntimeError,
-            "cannot reuse already awaited __anext__()/asend()");
-        return NULL;
-    }
-
-    if (o->ags_state == AWAITABLE_STATE_INIT) {
-        if (o->ags_gen->ag_running_async) {
-            o->ags_state = AWAITABLE_STATE_CLOSED;
+    int8_t state = FT_ATOMIC_LOAD_INT8_RELAXED(o->ags_state);
+    do {
+        if (state == AWAITABLE_STATE_CLOSED) {
             PyErr_SetString(
                 PyExc_RuntimeError,
-                "anext(): asynchronous generator is already running");
+                "cannot reuse already awaited __anext__()/asend()");
             return NULL;
         }
-
-        o->ags_state = AWAITABLE_STATE_ITER;
-        o->ags_gen->ag_running_async = 1;
+        if (state == AWAITABLE_STATE_ITER) {
+            goto do_throw;
+        }
+        assert(state == AWAITABLE_STATE_INIT);
+    } while (!_Py_ASYNC_GEN_TRY_SET_STATE(o->ags_state, state,
+                                          AWAITABLE_STATE_ITER));
+
+    // The transition above only guards this object, the generator may
+    // still be running through another asend()/athrow() object so
+    // try to claim it before running.
+    if (!async_gen_try_claim_running(o->ags_gen)) {
+        FT_ATOMIC_STORE_INT8_RELAXED(o->ags_state, AWAITABLE_STATE_CLOSED);
+        PyErr_SetString(
+            PyExc_RuntimeError,
+            "anext(): asynchronous generator is already running");
+        return NULL;
     }
 
-    PyObject *result = gen_throw((PyObject*)o->ags_gen, args, nargs);
+    PyObject *result;
+do_throw:
+    result = gen_throw((PyObject*)o->ags_gen, args, nargs);
     result = async_gen_unwrap_value(o->ags_gen, result);
 
     if (result == NULL) {
-        o->ags_gen->ag_running_async = 0;
-        o->ags_state = AWAITABLE_STATE_CLOSED;
+        FT_ATOMIC_STORE_INT8_RELAXED(o->ags_state, AWAITABLE_STATE_CLOSED);
+        FT_ATOMIC_STORE_INT8_RELEASE(o->ags_gen->ag_running_async, 0);
     }
 
     return result;
@@ -2069,7 +2130,7 @@ static PyObject *
 async_gen_asend_close(PyObject *self, PyObject *args)
 {
     PyAsyncGenASend *o = _PyAsyncGenASend_CAST(self);
-    if (o->ags_state == AWAITABLE_STATE_CLOSED) {
+    if (FT_ATOMIC_LOAD_INT8_RELAXED(o->ags_state) == AWAITABLE_STATE_CLOSED) {
         Py_RETURN_NONE;
     }
 
@@ -2304,80 +2365,100 @@ async_gen_athrow_send(PyObject *self, PyObject *arg)
     PyGenObject *gen = _PyGen_CAST(o->agt_gen);
     PyObject *retval;
 
-    if (o->agt_state == AWAITABLE_STATE_CLOSED) {
+    int8_t state = FT_ATOMIC_LOAD_INT8_RELAXED(o->agt_state);
+    if (state == AWAITABLE_STATE_CLOSED) {
         PyErr_SetString(
             PyExc_RuntimeError,
             "cannot reuse already awaited aclose()/athrow()");
         return NULL;
     }
 
-    if (FRAME_STATE_FINISHED(gen->gi_frame_state)) {
-        o->agt_state = AWAITABLE_STATE_CLOSED;
+    if (FRAME_STATE_FINISHED(FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state))) {
+        // Close the awaitable, unless another thread transitioned it
+        // to a different state in the meantime.
+        (void)_Py_ASYNC_GEN_TRY_SET_STATE(o->agt_state, state,
+                                          AWAITABLE_STATE_CLOSED);
         PyErr_SetNone(PyExc_StopIteration);
         return NULL;
     }
 
-    if (o->agt_state == AWAITABLE_STATE_INIT) {
-        if (o->agt_gen->ag_running_async) {
-            o->agt_state = AWAITABLE_STATE_CLOSED;
-            if (o->agt_typ == NULL) {
-                PyErr_SetString(
-                    PyExc_RuntimeError,
-                    "aclose(): asynchronous generator is already running");
-            }
-            else {
-                PyErr_SetString(
-                    PyExc_RuntimeError,
-                    "athrow(): asynchronous generator is already running");
-            }
+    do {
+        if (state == AWAITABLE_STATE_CLOSED) {
+            PyErr_SetString(
+                PyExc_RuntimeError,
+                "cannot reuse already awaited aclose()/athrow()");
             return NULL;
         }
-
-        if (o->agt_gen->ag_closed) {
-            o->agt_state = AWAITABLE_STATE_CLOSED;
-            PyErr_SetNone(PyExc_StopAsyncIteration);
-            return NULL;
+        if (state == AWAITABLE_STATE_ITER) {
+            goto do_send;
         }
-
+        assert(state == AWAITABLE_STATE_INIT);
         if (arg != Py_None) {
             PyErr_SetString(PyExc_RuntimeError, NON_INIT_CORO_MSG);
             return NULL;
         }
+    } while (!_Py_ASYNC_GEN_TRY_SET_STATE(o->agt_state, state,
+                                          AWAITABLE_STATE_ITER));
+
+    // The transition above only guards this object, the generator may
+    // still be running through another asend()/athrow() object so
+    // try to claim it before running.
+    if (!async_gen_try_claim_running(o->agt_gen)) {
+        FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+        if (o->agt_typ == NULL) {
+            PyErr_SetString(
+                PyExc_RuntimeError,
+                "aclose(): asynchronous generator is already running");
+        }
+        else {
+            PyErr_SetString(
+                PyExc_RuntimeError,
+                "athrow(): asynchronous generator is already running");
+        }
+        return NULL;
+    }
 
-        o->agt_state = AWAITABLE_STATE_ITER;
-        o->agt_gen->ag_running_async = 1;
+    if (FT_ATOMIC_LOAD_INT8_RELAXED(o->agt_gen->ag_closed)) {
+        FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+        FT_ATOMIC_STORE_INT8_RELEASE(o->agt_gen->ag_running_async, 0);
+        PyErr_SetNone(PyExc_StopAsyncIteration);
+        return NULL;
+    }
 
-        if (o->agt_typ == NULL) {
-            /* aclose() mode */
-            o->agt_gen->ag_closed = 1;
+    if (o->agt_typ == NULL) {
+        /* aclose() mode */
+        FT_ATOMIC_STORE_INT8_RELAXED(o->agt_gen->ag_closed, 1);
 
-            retval = _gen_throw((PyGenObject *)gen,
-                                0,  /* Do not close generator when
-                                       PyExc_GeneratorExit is passed */
-                                PyExc_GeneratorExit, NULL, NULL);
+        retval = _gen_throw((PyGenObject *)gen,
+                            0,  /* Do not close generator when
+                                   PyExc_GeneratorExit is passed */
+                            PyExc_GeneratorExit, NULL, NULL);
 
-            if (retval && _PyAsyncGenWrappedValue_CheckExact(retval)) {
-                Py_DECREF(retval);
-                goto yield_close;
-            }
-        } else {
-            retval = _gen_throw((PyGenObject *)gen,
-                                0,  /* Do not close generator when
-                                       PyExc_GeneratorExit is passed */
-                                o->agt_typ, o->agt_val, o->agt_tb);
-            retval = async_gen_unwrap_value(o->agt_gen, retval);
-        }
-        if (retval == NULL) {
-            goto check_error;
+        if (retval && _PyAsyncGenWrappedValue_CheckExact(retval)) {
+            Py_DECREF(retval);
+            goto yield_close;
         }
-        return retval;
+    } else {
+        retval = _gen_throw((PyGenObject *)gen,
+                            0,  /* Do not close generator when
+                                   PyExc_GeneratorExit is passed */
+                            o->agt_typ, o->agt_val, o->agt_tb);
+        retval = async_gen_unwrap_value(o->agt_gen, retval);
     }
+    if (retval == NULL) {
+        goto check_error;
+    }
+    return retval;
 
-    assert(o->agt_state == AWAITABLE_STATE_ITER);
-
+do_send:
     retval = gen_send((PyObject *)gen, arg);
     if (o->agt_typ) {
-        return async_gen_unwrap_value(o->agt_gen, retval);
+        retval = async_gen_unwrap_value(o->agt_gen, retval);
+        if (retval == NULL) {
+            FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+            FT_ATOMIC_STORE_INT8_RELEASE(o->agt_gen->ag_running_async, 0);
+        }
+        return retval;
     } else {
         /* aclose() mode */
         if (retval) {
@@ -2395,15 +2476,15 @@ async_gen_athrow_send(PyObject *self, PyObject *arg)
     }
 
 yield_close:
-    o->agt_gen->ag_running_async = 0;
-    o->agt_state = AWAITABLE_STATE_CLOSED;
+    FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+    FT_ATOMIC_STORE_INT8_RELEASE(o->agt_gen->ag_running_async, 0);
     PyErr_SetString(
         PyExc_RuntimeError, ASYNC_GEN_IGNORED_EXIT_MSG);
     return NULL;
 
 check_error:
-    o->agt_gen->ag_running_async = 0;
-    o->agt_state = AWAITABLE_STATE_CLOSED;
+    FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+    FT_ATOMIC_STORE_INT8_RELEASE(o->agt_gen->ag_running_async, 0);
     if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration) ||
             PyErr_ExceptionMatches(PyExc_GeneratorExit))
     {
@@ -2426,54 +2507,62 @@ async_gen_athrow_throw(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
 {
     PyAsyncGenAThrow *o = _PyAsyncGenAThrow_CAST(self);
 
-    if (o->agt_state == AWAITABLE_STATE_CLOSED) {
-        PyErr_SetString(
-            PyExc_RuntimeError,
-            "cannot reuse already awaited aclose()/athrow()");
-        return NULL;
-    }
-
-    if (o->agt_state == AWAITABLE_STATE_INIT) {
-        if (o->agt_gen->ag_running_async) {
-            o->agt_state = AWAITABLE_STATE_CLOSED;
-            if (o->agt_typ == NULL) {
-                PyErr_SetString(
-                    PyExc_RuntimeError,
-                    "aclose(): asynchronous generator is already running");
-            }
-            else {
-                PyErr_SetString(
-                    PyExc_RuntimeError,
-                    "athrow(): asynchronous generator is already running");
-            }
+    int8_t state = FT_ATOMIC_LOAD_INT8_RELAXED(o->agt_state);
+    do {
+        if (state == AWAITABLE_STATE_CLOSED) {
+            PyErr_SetString(
+                PyExc_RuntimeError,
+                "cannot reuse already awaited aclose()/athrow()");
             return NULL;
         }
-
-        o->agt_state = AWAITABLE_STATE_ITER;
-        o->agt_gen->ag_running_async = 1;
+        if (state == AWAITABLE_STATE_ITER) {
+            goto do_throw;
+        }
+        assert(state == AWAITABLE_STATE_INIT);
+    } while (!_Py_ASYNC_GEN_TRY_SET_STATE(o->agt_state, state,
+                                          AWAITABLE_STATE_ITER));
+
+    // The transition above only guards this object, the generator may
+    // still be running through another asend()/athrow() object so
+    // try to claim it before running.
+    if (!async_gen_try_claim_running(o->agt_gen)) {
+        FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+        if (o->agt_typ == NULL) {
+            PyErr_SetString(
+                PyExc_RuntimeError,
+                "aclose(): asynchronous generator is already running");
+        }
+        else {
+            PyErr_SetString(
+                PyExc_RuntimeError,
+                "athrow(): asynchronous generator is already running");
+        }
+        return NULL;
     }
 
-    PyObject *retval = gen_throw((PyObject*)o->agt_gen, args, nargs);
+    PyObject *retval;
+do_throw:
+    retval = gen_throw((PyObject*)o->agt_gen, args, nargs);
     if (o->agt_typ) {
         retval = async_gen_unwrap_value(o->agt_gen, retval);
         if (retval == NULL) {
-            o->agt_gen->ag_running_async = 0;
-            o->agt_state = AWAITABLE_STATE_CLOSED;
+            FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+            FT_ATOMIC_STORE_INT8_RELEASE(o->agt_gen->ag_running_async, 0);
         }
         return retval;
     }
     else {
         /* aclose() mode */
         if (retval && _PyAsyncGenWrappedValue_CheckExact(retval)) {
-            o->agt_gen->ag_running_async = 0;
-            o->agt_state = AWAITABLE_STATE_CLOSED;
+            FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+            FT_ATOMIC_STORE_INT8_RELEASE(o->agt_gen->ag_running_async, 0);
             Py_DECREF(retval);
             PyErr_SetString(PyExc_RuntimeError, ASYNC_GEN_IGNORED_EXIT_MSG);
             return NULL;
         }
         if (retval == NULL) {
-            o->agt_gen->ag_running_async = 0;
-            o->agt_state = AWAITABLE_STATE_CLOSED;
+            FT_ATOMIC_STORE_INT8_RELAXED(o->agt_state, AWAITABLE_STATE_CLOSED);
+            FT_ATOMIC_STORE_INT8_RELEASE(o->agt_gen->ag_running_async, 0);
         }
         if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration) ||
             PyErr_ExceptionMatches(PyExc_GeneratorExit))
@@ -2502,7 +2591,7 @@ static PyObject *
 async_gen_athrow_close(PyObject *self, PyObject *args)
 {
     PyAsyncGenAThrow *agt = _PyAsyncGenAThrow_CAST(self);
-    if (agt->agt_state == AWAITABLE_STATE_CLOSED) {
+    if (FT_ATOMIC_LOAD_INT8_RELAXED(agt->agt_state) == AWAITABLE_STATE_CLOSED) {
         Py_RETURN_NONE;
     }
     PyObject *result = async_gen_athrow_throw((PyObject*)agt,

_______________________________________________
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.