مقدمه
تا اینجا دو روش برای همزمانی در پایتون دیدیم: «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 میپردازیم.