در asyncio، گرفتنِ خطایِ یک کار همیشه به معنیِ پایانِ کارهایِ همزمانِ دیگر نیست. TaskGroup هنگامِ خطایِ عادیِ یک فرزند، لغو و انتظارِ دیگران را در عمرِ گروه مدیریت میکند؛ gather پیشفرض رفتارِ متفاوتی دارد. مرزِ catch و خروج از گروه را با دو coroutine بیاثر و رویدادِ صریح میسنجیم.
کارگر منتظر است، کارِ دوم خطا میدهد
کارگرِ اصیل ابتدا started را set میکند و بعد منتظرِ release میماند. در finally رویدادِ cleanup را ثبت میکند. کارِ دوم منتظرِ started میشود و آنگاه ValueError ساختگی میدهد. هیچ sleep، شبکه، فایل، صفِ سرور یا تماسِ واقعی در این سناریو نیست.
این ترتیب با Event ها کنترل شده تا خطا پیش از شروعِ کارگر رخ ندهد. به فاصلهٔ میلیثانیه یا خوششانسیِ زمانبندی تکیه نداریم. دو دستهٔ یکسان از کارها در TaskGroup و gather اجرا میشوند؛ گزارشِ هرکدام handle هایِ کارها و رویدادهایِ مشاهدهشده را نگه میدارد.
خروج از TaskGroup پس از cleanup است
با خطایِ عادیِ فرزند، کارگرِ منتظر لغو میشود. finally آن واقعاً اجرا میشود و تنها بعد از آن، بیرونِ async with، ExceptionGroup در except*گرفته میشود. نوعِ برگِ استثنا ValueError است؛ در آن مرز هر دو فرزند done هستند و کارگر cancelled است.
پس تضمینِ قابلِ آزمایشِ این مثال فقط نمایشِ خطا نیست: رویدادِ worker_cleanup باید پیش از group_exception_caught_after_exit باشد. فایل، هم ترتیبِ دو رویداد و هم وضعیتِ Task ها را assert میکند. این نتیجه به پذیرشِ تعاونیِ لغو توسطِ coroutine وابسته است.
gather خطا را میدهد، کارگر هنوز زنده است
در لحظهٔ catch ValueError از gather پیشفرض، کارگرِ دیگر done نیست، cancelled نیست و cleanup آن هنوز رخ نداده است. این سه مشاهده در خروجی ثبت شدهاند. گرفتنِ استثنا را نباید پایانِ همهٔ کارها معرفی کرد؛ برنامه هنوز مسئولِ عمرِ handle باقیمانده است.
مهارِ آزمایش در finally کارگر را صریح cancel و await میکند. سپس handle کارِ خطاداده نیز جمع میشود و هیچ Task تازهٔ زندهای پس از آزمایش نمیماند. بنابراین برای نشاندادنِ تفاوت، عمداً کارِ رهاشده در event loop نگه نداشتهایم.
| مرزِ مشاهده | TaskGroup | gather پیشفرض |
|---|---|---|
| catch خطای عادی | کارگر لغو و cleanup شده | کارگر هنوز منتظر |
| پایانِ مهارِ آزمایش | هر دو done | پس از cancel و await صریح، هر دو done |
خطایِ cleanup هم نباید پنهان شود
در سناریویِ جدا، finally کارگر پس از لغو RuntimeError ساختگی میدهد. گروهِ نهایی هر دو برگِ RuntimeError و ValueError را دارد؛ کارگر اکنون خطاداده است و cancelled نیست. تبدیلِ همهٔ این وضعیتها به «لغو شد» علتِ دوم را پنهان میکند.
دو شاهدِ دیگر واقعاً اجرا میشوند: پایانِ موفق با پاسخهای ۴ و ۹، و فرزندی که CancelledError میدهد در کنارِ خواهرِ موفقِ ۲۵. در حالتِ دوم، CancelledError همان فرزند بهتنهایی موجبِ لغوِ خواهرِ موفق نشده است. CancelledError را نباید بیبررسی مانندِ هر Exception عادی گرفت و بلعید.
لغوِ گروه، تراکنش نیست
لغوِ خودِ gather درحالِ انتظار قراردادِ دیگری باخطایِ عادیِ یک فرزند دارد. همچنین gather.cancel پس از آنکه باانتقالِ خطا done شده، باقیماندهها را خودکار لغو نمیکند. کدِ ما برای کنترلِ صریحِ آنها از handle استفاده میکند؛ سناریویِ لغوِ والد جداگانه اینجا اجرا نشده است.
کل مجموعه دوبار با Python۳٫۱۲٫۱۴ اجرا و JSON ها برابرند. بلعیدنِ CancelledError، کارِ CPU همگام، kill کردنِ thread، timeout و rollback اثرِ بیرونی در این پوشش پشتیبانی یا تضمین نمیشوند. لغوِ کارِ سرور مالکیت و نقطهٔ تعهد و timeout و retry تکرارِ اثر را میسنجد. ادامه در مبانی هوش مصنوعی؛ این مثال عمرِ coroutine است، نه قراردادِ داخلیِ اپرا.
فایل تمرین و منبع
کد کاملِ تمرینِ مستقل و کنترلها؛ Python · ورودی و تمامِ خروجیِ اجرای واقعی؛ JSON
منابع در 2026-11-04 خوانده شدند؛ این تاریخ، تاریخ انتشار منبع نیست. مثالها و فایلهای این راهنما آموزشی و ساختهٔ تحریریهاند.