این قطعه کد را نگاه کنید؛ بهظاهر میخواهد به محض آماده شدن اولین Task، جلو برود، اما یک خطای زشت میگیرد:
import asyncio
async def fetch(name, delay):
await asyncio.sleep(delay)
return name
async def main():
# WRONG: passing raw coroutines, not Tasks
done, pending = await asyncio.wait({fetch('a', 1), fetch('b', 2)})
print(done)
asyncio.run(main())
در پایتونهای امروزی این کد با خطای TypeError: Passing coroutines is forbidden متوقف میشود. چرا؟ چون تابع asyncio.wait فقط با شیءهای Task کار میکند، نه با Coroutine خام. این اولین و رایجترین دامی است که هنگام کار با توابع wait و wait_for در آن میافتید. در این درس اول تابع سادهٔ wait_for را میبینیم و بعد سراغ wait میرویم که قدرت واقعی برای کنترل چند Task همزمان است.
مشکل: یک عملیات کند که نباید تا ابد منتظرش بمانیم
تصور کنید به یک سرویس شبکه درخواست میزنید و آن سرویس ممکن است کند باشد یا اصلاً جواب ندهد. نمیخواهید برنامهتان برای همیشه معلق بماند. اینجا asyncio.wait_for(coro, timeout) وارد میشود: یک Coroutine یا Task را اجرا میکند و حداکثر timeout ثانیه صبر میکند. اگر کار سر وقت تمام نشد، خطای asyncio.TimeoutError پرتاب میشود و عملیات بهطور خودکار لغو میگردد.
import asyncio
async def slow_operation():
await asyncio.sleep(5) # pretend this is a slow network call
return 'result'
async def main():
try:
# wait at most 2 seconds, then give up
result = await asyncio.wait_for(slow_operation(), timeout=2)
print(result)
except asyncio.TimeoutError:
print('too slow, operation was cancelled')
asyncio.run(main())
چون slow_operation پنج ثانیه میخواهد ولی فقط دو ثانیه فرصت داریم، پیام لغو چاپ میشود. نکتهٔ مهمی که خیلیها فراموش میکنند: با رخ دادن Timeout، خودِ عملیات هم cancel میشود؛ یعنی نه فقط دست از انتظار میکشید، بلکه کار در حال اجرا هم متوقف میگردد.
تابع wait: منتظر اولین جوابِ چند آینه بمان
حالا سناریوی واقعیتر: یک فایل روی چند آینهٔ (mirror) دانلود در دسترس است و شما میخواهید به محض اینکه هر کدام اول جواب داد، بقیه را رها کنید. اینجا wait_for کافی نیست چون فقط یک هدف دارد؛ به asyncio.wait نیاز داریم که روی مجموعهای از Taskها کار میکند و دو مجموعه برمیگرداند: done (تمامشدهها) و pending (هنوز در حال اجرا).
import asyncio
async def mirror(name, delay):
await asyncio.sleep(delay) # each mirror answers at a different speed
return f'{name} responded'
async def main():
# create_task turns coroutines into real Tasks (required by wait)
tasks = {
asyncio.create_task(mirror('mirror-A', 3)),
asyncio.create_task(mirror('mirror-B', 1)),
asyncio.create_task(mirror('mirror-C', 2)),
}
done, pending = await asyncio.wait(
tasks,
return_when=asyncio.FIRST_COMPLETED # return as soon as one finishes
)
for task in done:
print(task.result())
# we already have a winner, cancel the rest
for task in pending:
task.cancel()
asyncio.run(main())
سریعترین آینه (B با یک ثانیه) داخل done مینشیند و بلافاصله wait برمیگردد. بقیه در pending میمانند و ما دستی آنها را cancel میکنیم. برخلاف wait_for، تابع wait هرگز خودش خطا پرتاب نمیکند و Taskهای ناتمام را هم لغو نمیکند؛ این تصمیم را کاملاً به شما میسپارد.
پارامتر return_when
این پارامتر تعیین میکند wait کِی برگردد:
ALL_COMPLETED(پیشفرض): تا پایان همهٔ Taskها صبر میکند.FIRST_COMPLETED: به محض تمام شدن اولین Task برمیگردد.FIRST_EXCEPTION: اگر Taskی خطا بدهد فوراً برمیگردد، وگرنه تا پایان همه صبر میکند.
دام رایج و دلیلش
همان خطای ابتدای درس: به wait هرگز Coroutine خام ندهید. دلیلش این است که wait باید بتواند وضعیت هر آیتم را جداگانه دنبال کند و لغوشان کند، و این فقط با شیء Task ممکن است؛ پس اول با asyncio.create_task بسازید. دام دوم درباره wait_for است: اگر داخل عملیاتِ محافظتنشده کاری حیاتی انجام میدهید، لغو خودکار پس از Timeout میتواند آن را نیمهکاره رها کند؛ برای این حالت باید سراغ asyncio.shield بروید که در درس بعد میبینیم.
کِی از کدام استفاده کنیم؟
اگر یک عملیات تکی دارید و فقط میخواهید سقف زمانی بگذارید، wait_for ساده و کافی است. اما اگر چند Task دارید و میخواهید بر اساس اولین جواب یا اولین خطا تصمیم بگیرید و کنترل لغو در دست خودتان باشد، wait ابزار درست است.
جمعبندی
wait_forبرای یک عملیات با سقف زمانی است و در Timeout هم انتظار و هم خود عملیات را لغو میکند.waitروی مجموعهای از Taskها کار میکند، دو مجموعهٔdoneوpendingمیدهد و هیچچیز را خودش لغو نمیکند.- همیشه قبل از
wait، Coroutine را باcreate_taskبه Task تبدیل کنید. - با
FIRST_COMPLETEDمیتوانید الگوی «سریعترین برنده» را برای آینهها و درخواستهای موازی پیاده کنید.