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]

Reply via email to