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

Please login to bookmark Close

مقدمه

تا اینجا دو روش برای هم‌زمانی در پایتون دیدیم: «Threading» و «Multiprocessing». روش سومی هم وجود دارد که این روزها در پروژه‌های شبکه‌محور (مثل درخواست‌های HTTP، اتصال به دیتابیس و کار با فایل) خیلی محبوب است: «asyncio».

برخلاف Threading و Multiprocessing، در asyncio فقط یک Thread و یک Process داریم. هم‌زمانی با یک «حلقه‌ی رویداد» یا «Event Loop» به‌دست می‌آید: وقتی یک تسک منتظر چیزی است (مثلاً پاسخ شبکه)، به‌جای بلاک‌شدن، کنترل را به Event Loop برمی‌گرداند تا تسک دیگری اجرا شود. به همین دلیل به این مدل «Cooperative Multitasking» می‌گویند.

چون فقط یک Thread درگیر است، asyncio برای کارهای CPU-bound کمکی نمی‌کند؛ اما برای کارهای I/O-bound که زمان زیادی صرف «منتظر ماندن» می‌شود، می‌تواند بسیار سریع‌تر و کم‌هزینه‌تر از Threading عمل کند.

یادآوری: coroutine و await

تابعی که با «async def» تعریف شود، به آن «coroutine function» می‌گویند. صدا زدن این تابع، کد داخلش را اجرا نمی‌کند؛ فقط یک «coroutine object» برمی‌گرداند. برای اجرای واقعی آن باید از «await» (داخل یک coroutine دیگر) یا «asyncio.run()» (برای شروع برنامه) استفاده کنیم.

import asyncio

async def say_hello(name):
    print(f'Hello, {name}!')

async def main():
    await say_hello('Ali')

asyncio.run(main())

این کد کاملاً ترتیبی اجرا می‌شود؛ هیچ هم‌زمانی‌ای در آن نیست. برای هم‌زمانی واقعی به «Task» نیاز داریم.

Task چیست؟

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

import asyncio

async def say_hello(name):
    print(f'Hello, {name}!')

async def main():
    task = asyncio.create_task(say_hello('Ali'))
    await task

asyncio.run(main())

در این مثال هنوز تفاوت زیادی با حالت قبل دیده نمی‌شود، چون فقط یک Task داریم. تفاوت اصلی وقتی مشخص می‌شود که چند Task را هم‌زمان اجرا کنیم.

مثال: اجرای هم‌زمان چند Task

نکته‌ی مهم این است که باید همه‌ی Taskها را اول بسازیم و بعد یکی‌یکی منتظرشان بمانیم؛ نه اینکه هر بار بسازیم و فوراً await کنیم. در حالت دوم، عملاً ترتیبی اجرا می‌شوند.

import asyncio

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

async def main():
    task1 = asyncio.create_task(fetch_data('Task 1', 2))
    task2 = asyncio.create_task(fetch_data('Task 2', 3))
    await task1
    await task2

asyncio.run(main())

با اینکه «Task 2» به اندازه‌ی بیشتری منتظر می‌ماند، چون هر دو Task هم‌زمان شروع شده‌اند، کل برنامه تقریباً به اندازه‌ی طولانی‌ترین تسک طول می‌کشد؛ نه مجموع زمان هر دو. این دقیقاً همان مزیتی است که Task به ما می‌دهد.

متدهای مهم Task

شیء Task چند متد کاربردی دارد:

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

لغو کردن یک Task

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

import asyncio

async def long_task():
    delay = 10
    print('long_task: started')
    await asyncio.sleep(delay)
    print('long_task: finished')

async def main():
    task = asyncio.create_task(long_task())
    short_delay = 1
    await asyncio.sleep(short_delay)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print('long_task: cancelled')
    print(f'task.cancelled(): {task.cancelled()}')

asyncio.run(main())

در این مثال «long_task» باید ۱۰ ثانیه طول بکشد، اما بعد از حدود ۱ ثانیه آن را لغو می‌کنیم. به همین خاطر پیام «finished» هیچ‌وقت چاپ نمی‌شود و در عوض با خطای «CancelledError» مواجه می‌شویم که آن را با «try/except» می‌گیریم.

جمع‌بندی

«Task» راه استاندارد برای زمان‌بندی هم‌زمان چند coroutine روی Event Loop است. کافی است coroutine را داخل «asyncio.create_task()» بپوشانیم تا فوراً شروع به اجرا کند، بدون نیاز به await فوری. با متدهایی مثل «done()»، «result()» و «cancel()» می‌توانیم وضعیت هر Task را زیر نظر داشته باشیم یا در صورت نیاز آن را متوقف کنیم. در درس‌های بعدی به ابزارهای دیگر asyncio مثل Queue، Semaphore و Event می‌پردازیم.

Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (55%)
نظرات

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

55%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

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

پاسخ تمرین ها

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

اشتراک گذاری

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

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

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

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

تنظیمات

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