آموزش ماژول asyncio – توابع wait و wait_for

Please login to bookmark Close

این قطعه کد را نگاه کنید؛ به‌ظاهر می‌خواهد به محض آماده شدن اولین 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 می‌توانید الگوی «سریع‌ترین برنده» را برای آینه‌ها و درخواست‌های موازی پیاده کنید.
Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (64%)
نظرات

دیدگاهتان را بنویسید

64%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم