Free-threading: concurrent send() on a shared aclose() awaitable can resume the async generator instead of closing it
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 38/100
Hướng nghiên cứu
Start in Objects/genobject.c at async_gen_athrow_send(), especially the INIT-to-ITER transition, ag_running_async claim, and aclose _gen_throw() path described in the report. Compare the shared aclose and athrow paths, then run test_free_threading.test_async_generators.test_concurrent_shared_aclose and the related shared athrow test on a free-threaded debug build. Done when concurrent sends reliably close or throw into the generator and its cleanup runs.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Bug report
Bug description:
On the free-threaded build (--disable-gil --with-pydebug), test_free_threading.test_async_generators.test_concurrent_shared_aclose fails intermittently:
File "Lib/test/test_free_threading/test_async_generators.py", line 171, in test_concurrent_shared_aclose
self.assertEqual(cleanups, [1])
AssertionError: Lists differ: [] != [1]
Both send() calls on the shared aclose() awaitable raise (the test ignores RuntimeError/ValueError), and the generator's finally block never runs.
I think the cause is this window in async_gen_athrow_send():
- Thread T1 moves
agt_statefrom INIT to ITER (L2434-L2435), claimsag_running_async(L2440) and setsag_closed(L2464), but has not yet called_gen_throw(GeneratorExit)(L2466-L2469). - Thread T2 sees ITER and takes the
do_sendpath (L2427, L2488), which callsgen_send(gen, None)without claimingag_running_async. The generator is resumed withNoneinstead ofGeneratorExit, runs on and yields again, and T2 getsRuntimeError: async generator ignored GeneratorExit. - Meanwhile, T1's
_gen_throw()finds the frame executing and fails withValueError: async generator already executing.
With the GIL there is no switch point between the state change and _gen_throw(), so this cannot happen there. The agt_state CAS and the separate ag_running_async claim were added in gh-120321 (61818b6087); the ITER → gen_send() path itself is older.
Deterministic reproducer: widen the window with a temporary delay before the _gen_throw() call in aclose mode (debug only, not a fix):
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -3,6 +3,7 @@
#define _PY_INTERPRETER
#include "Python.h"
+#include <unistd.h> // TEMPORARY: usleep()
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_ceval.h" // _PyEval_EvalFrame()
#include "pycore_frame.h" // _PyInterpreterFrame
@@ -2462,6 +2463,9 @@
if (o->agt_typ == NULL) {
/* aclose() mode */
FT_ATOMIC_STORE_INT8_RELAXED(o->agt_gen->ag_closed, 1);
+ if (getenv("PY_ACLOSE_DELAY_US")) { /* TEMPORARY: widen race window */
+ usleep((useconds_t)atoi(getenv("PY_ACLOSE_DELAY_US")));
+ }
retval = _gen_throw((PyGenObject *)gen,
0, /* Do not close generator when
then run the following. The time.sleep(0.3) keeps T2 inside the generator while T1 calls _gen_throw():
# Two threads call send(None) on ONE shared aclose() awaitable, 20 ms apart.
import threading, time
def make(cleanups):
async def agen():
try:
while True:
yield 1
time.sleep(0.3) # reached only if resumed by send(None), not by aclose()
finally:
cleanups.append(1)
return agen()
for trial in range(5):
cleanups = []
ag = make(cleanups)
try:
ag.asend(None).send(None) # advance to the first yield
except StopIteration:
pass
aw = ag.aclose()
res = {}
def worker(name):
try:
aw.send(None)
res[name] = "returned"
except BaseException as e:
res[name] = f"{type(e).__name__}: {e}"
t1 = threading.Thread(target=worker, args=("T1",))
t2 = threading.Thread(target=worker, args=("T2",))
t1.start(); time.sleep(0.02); t2.start(); t1.join(); t2.join()
print(trial, res, "cleanups:", cleanups)
With PY_ACLOSE_DELAY_US=100000 every trial fails:
0 {'T1': 'ValueError: async generator already executing', 'T2': 'RuntimeError: async generator ignored GeneratorExit'} cleanups: []
Without the variable all trials pass (cleanups: [1]).
Without any patch, running test_concurrent_shared_aclose in a loop in 8 parallel processes failed 12 times in 800,000 runs on main (1705e512b30), and 25 times in 800,000 on 5d887642d12 (just before gh-158304, which also touched async generators), so it is not a new regression. In the same setup test_concurrent_shared_athrow failed 17 times in 400,000 runs with StopIteration: 1 from the asend() check at line 142, so the generator was still alive: it was resumed instead of receiving the thrown ValueError. It goes through the same function, so I expect it is the same window. test_concurrent_shared_asend did not fail in 400,000 runs.
CPython versions tested on:
CPython main branch
Operating systems tested on:
macOS
- Ngôn ngữ chính
- Python
- Star
- 77.7k
- Fork
- 38.3k
- Merge trung bình
- 13 giờ 43 phút
- Pull request đã merge (30 ngày)
- 704
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của python/cpython
-
topic-socket type-bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
python/cpython#160561 · 7 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
tests topic-profiling type-bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
python/cpython#160334 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
`iso2022_jp_3` encodes U+9B1D but decodes the result as U+9B1CCó thể đã có người làm @kinqsradio đã nhận hôm nay. Đang mởextension-modules type-bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Remove unused FVS_* macrosĐang mởinterpreter-core type-feature
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 78/100
python/cpython#160109 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
extension-modules type-bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
python/cpython#160001 · 3 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của python/cpython
Issue tương tự
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 82/100
btclib-org/btclib-node#2083 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
Gathin23/cleanyourtv#2089 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
jxxghp/MoviePilot#7212 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Web transport: small test gapsĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
Maintainer thường phản hồi trong vòng 1 ngày
-
cli dx rust
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
NekomyaDev/nudge#278 ·
Maintainer thường phản hồi trong vòng 1 ngày