مقدمه
در درسهای قبلی با asyncio.gather() برای اجرای همزمان چند Task آشنا شدیم. اما گاهی به کنترل دقیقتری نیاز داریم؛ مثلاً میخواهیم اگر یک عملیات بیش از حد معینی طول کشید، آن را لغو کنیم، یا میخواهیم به محض پایان یافتن اولین Task از بین چند Task، نتیجه را بررسی کنیم. دو تابع wait_for و wait دقیقاً برای این نوع سناریوها طراحی شدهاند.
تابع wait_for
تابع asyncio.wait_for(coro, timeout) یک Coroutine یا Task را اجرا میکند و حداکثر تا مدتزمان timeout (به ثانیه) منتظر پایان آن میماند. اگر در این مدت کار به پایان نرسد، خطای asyncio.TimeoutError رخ میدهد و Task مربوطه بهطور خودکار لغو میشود.
import asyncio
async def slow_operation():
delay = 5
await asyncio.sleep(delay)
return 'نتیجهٔ عملیات'
async def main():
try:
timeout = 2
result = await asyncio.wait_for(slow_operation(), timeout=timeout)
print(result)
except asyncio.TimeoutError:
print('عملیات بیش از حد طول کشید و لغو شد.')
asyncio.run(main())در این مثال، چون slow_operation به ۵ ثانیه زمان نیاز دارد ولی فقط ۲ ثانیه فرصت داده شده، خطای TimeoutError رخ میدهد و پیام «عملیات بیش از حد طول کشید و لغو شد» چاپ خواهد شد.
تابع wait
تابع asyncio.wait(tasks, timeout=None, return_when=…) برخلاف wait_for، روی مجموعهای از Taskها کار میکند و دو مجموعه به نام done و pending برمیگرداند: مجموعهٔ done شامل Taskهایی است که کارشان تمام شده، و pending شامل Taskهایی است که هنوز در حال اجرا هستند. این تابع بهجای ایجاد خطا، تصمیمگیری دربارهٔ Taskهای ناتمام را به عهدهٔ برنامهنویس میگذارد.
پارامتر return_when تعیین میکند که wait چه زمانی باید برگردد؛ سه مقدار رایج برای آن وجود دارد:
- asyncio.ALL_COMPLETED (پیشفرض): «صبر میکند تا همهٔ Taskها به پایان برسند.»
- asyncio.FIRST_COMPLETED: «به محض پایان یافتن اولین Task (چه موفق چه با خطا)، برمیگردد.»
- asyncio.FIRST_EXCEPTION: «اگر یکی از Taskها خطا بدهد فوراً برمیگردد؛ در غیر این صورت تا پایان همهٔ Taskها صبر میکند.»
import asyncio
async def fetch_data(name, delay):
await asyncio.sleep(delay)
return f'{name}: داده دریافت شد'
async def main():
task1 = asyncio.create_task(fetch_data('Task 1', 2))
task2 = asyncio.create_task(fetch_data('Task 2', 4))
done, pending = await asyncio.wait(
{task1, task2},
return_when=asyncio.FIRST_COMPLETED
)
for task in done:
print(task.result())
print(f'تعداد Task های هنوز در حال اجرا: {len(pending)}')
for task in pending:
task.cancel()
asyncio.run(main())نکتهٔ مهم: تابع wait باید با Taskها (یعنی خروجی create_task) کار کند، نه با Coroutine خام؛ از نسخههای جدید پایتون، ارسال مستقیم Coroutine به این تابع دیگر مجاز نیست و باید پیش از آن، با create_task آن را به Task تبدیل کرد.
تفاوت wait_for و wait
- wait_for(coro, timeout): «روی یک Coroutine یا Task تکی کار میکند و در صورت Timeout شدن، خطای TimeoutError ایجاد میکند و آن را بهطور خودکار لغو میکند.»
- wait(tasks, timeout, return_when): «روی مجموعهای از Taskها کار میکند و بهجای ایجاد خطا، دو مجموعهٔ done و pending را برمیگرداند؛ لغو Taskهای pending به عهدهٔ برنامهنویس است.»
جمعبندی
با wait_for میتوان برای یک عملیات تکی محدودیت زمانی تعیین کرد، و با wait میتوان رفتار دقیقتری برای مجموعهای از Taskها پیادهسازی کرد؛ مثلاً واکنش به اولین Task کاملشده بدون نیاز به صبر برای بقیه. در درس بعدی به سراغ کلاس Semaphore در asyncio میرویم تا یاد بگیریم چگونه تعداد Taskهای همزمانِ فعال را محدود کنیم.