آموزش ماژول asyncio – Task و متد create_task

Please login to bookmark Close

این دو تکه کد را مقایسه کنید. هر دو دو کوروتین را اجرا می‌کنند که هر کدام ۳ ثانیه می‌خوابند، اما یکی ۶ ثانیه طول می‌کشد و دیگری حدود ۳ ثانیه. تفاوت در یک جمله است: اینکه کجا await می‌کنید. همین تفاوت ریز ما را به قلب موضوع این درس می‌برد: شیء Task و متد create_task، که کلید واقعی همزمانی در asyncio هستند.

اشتباه رایج: await پشت سر هم

خیلی‌ها تصور می‌کنند هر await یعنی «همزمانی»، اما این‌طور نیست. وقتی یک کوروتین را مستقیم await کنید، دقیقاً همان‌جا می‌ایستید تا تمام شود، بعد سراغ خط بعدی می‌روید. پس کد زیر کاملاً ترتیبی است:

import asyncio

async def fetch_data(name, delay):
    print(f'{name}: started')
    await asyncio.sleep(delay)
    print(f'{name}: finished')

async def main():
    # each await blocks here until the coroutine finishes -> sequential
    await fetch_data('Task 1', 3)
    await fetch_data('Task 2', 3)

asyncio.run(main())  # takes about 6 seconds

این کد حدود ۶ ثانیه طول می‌کشد، که هیچ بهتر از کد سینکرون نیست. در درس run_in_executor هم دیدیم که فقط داشتن async کافی نیست؛ مهم این است که حلقهٔ رویداد فرصت سوئیچ داشته باشد. برای همین به Task نیاز داریم.

Task چیست؟ نقش create_task

وقتی یک کوروتین را داخل asyncio.create_task() می‌پیچید، آن کوروتین بلافاصله روی حلقهٔ رویداد زمان‌بندی می‌شود تا اجرا شود، بدون اینکه لازم باشد همان لحظه await کنید. خروجی آن یک شیء Task است که می‌توانید بعداً منتظرش بمانید یا وضعیتش را بررسی کنید. نکتهٔ کلیدی همین جداشدن «شروع کردن» از «منتظر ماندن» است.

async def main():
    # both tasks are scheduled and start running right away
    task1 = asyncio.create_task(fetch_data('Task 1', 3))
    task2 = asyncio.create_task(fetch_data('Task 2', 3))
    # now we just wait for the already-running tasks
    await task1
    await task2

asyncio.run(main())  # takes about 3 seconds, not 6

این بار هر دو Task قبل از رسیدن به خط await شروع شده‌اند، پس همزمان می‌خوابند و کل برنامه تقریباً به اندازهٔ طولانی‌ترین Task طول می‌کشد، نه مجموع آن‌ها. این دقیقاً همان مزیتی است که Task به ما می‌دهد.

مثال واقعی: گرفتن چند API همزمان

تصور کنید یک داشبورد دارید که باید قیمت ارز، آب‌وهوا و آخرین اخبار را از سه سرویس مختلف بگیرد. اگر پشت سر هم await کنید، کاربر باید مجموع زمان هر سه درخواست را صبر کند. با create_task هر سه را همزمان می‌فرستیم. در عمل برای این الگو معمولاً asyncio.gather راحت‌تر است، که درون خود همین Task‌ها را می‌سازد:

async def main():
    # gather schedules all three concurrently and collects their results
    prices, weather, news = await asyncio.gather(
        fetch_data('prices', 2),
        fetch_data('weather', 1),
        fetch_data('news', 3),
    )

متدهای مهم شیء Task

  • done(): مشخص می‌کند Task به پایان رسیده یا نه.
  • cancel(): درخواست لغو Task را می‌فرستد.
  • result(): مقدار بازگشتی Task را برمی‌گرداند (اگر تمام شده باشد).
  • exception(): اگر Task با خطا متوقف شده باشد، آن exception را می‌دهد.

لغو کردن یک Task

گاهی لازم است یک Task را قبل از پایان طبیعی‌اش متوقف کنیم؛ مثلاً وقتی کاربر صفحه را می‌بندد یا یک timeout می‌گذاریم. متد cancel() در اولین نقطهٔ await داخل کوروتین، خطای asyncio.CancelledError را پرتاب می‌کند.

async def long_task():
    print('long_task: started')
    await asyncio.sleep(10)
    print('long_task: finished')  # never reached

async def main():
    task = asyncio.create_task(long_task())
    await asyncio.sleep(1)
    task.cancel()  # ask the task to stop
    try:
        await task  # CancelledError surfaces here
    except asyncio.CancelledError:
        print('long_task: cancelled')
    print(f'task.cancelled(): {task.cancelled()}')

asyncio.run(main())

یک تلهٔ رایج اینجاست: اگر کوروتین CancelledError را بگیرد و دوباره پرتابش نکند (یعنی جلوی لغو را بگیرد)، عملاً درخواست لغو را خنثی کرده‌اید؛ چون این خطا همان سازوکاری است که لغو را به کوروتین اطلاع می‌دهد. اگر برای پاک‌سازی منابع آن را می‌گیرید، حتماً در انتها دوباره raise کنید.

جمع‌بندی

  • await پشت سر هم روی کوروتین‌ها، اجرا را ترتیبی می‌کند و هیچ همزمانی به شما نمی‌دهد.
  • create_task کوروتین را فوراً روی حلقهٔ رویداد زمان‌بندی می‌کند؛ اول همه را بسازید، بعد await کنید.
  • با done()، result() و cancel() وضعیت Task را مدیریت کنید؛ و CancelledError را بی‌دلیل خنثی نکنید.
  • برای اجرای گروهی و جمع‌آوری نتیجه‌ها، asyncio.gather راحت‌تر از مدیریت دستی چند Task است.
Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (55%)
نظرات

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

55%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

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

پاسخ تمرین ها

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

اشتراک گذاری

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

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

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

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

تنظیمات

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