
coroutine چیست؟
همروال (coroutine) نوعی تابع است که معمولاً حداقل یک درخواست به منابع خارجی دارد و پاسخگرفتن به آن درخواست زمان قابلتوجهی طول میکشد (مثل فراخوانی یک API یا خواندن از شبکه). ویژگی خاص coroutine این است که میتواند در میانهی اجرا «مکث» کند، کنترل را به دیگری بدهد، و بعداً از همان نقطه ادامه یابد.
چرا باید از coroutine استفاده کنیم؟
فرض کنید یک تابع معمولی ۲۰ خطی دارید. نمیتوانید اجرای آن را در خط ۱۰ متوقف کنید، تابع دیگری را اجرا کنید و بعد از خط ۱۱ ادامه دهید. اما coroutine دقیقاً همین امکان را میدهد. حالا چرا اصلاً باید در اجرای یک تابع وقفه ایجاد کرد؟
اگر فراخوانی یک API که پاسخش چند ثانیه طول میکشد را در یک تابع معمولی انجام دهید، در همان نقطه ترد «قفل» میشود و بیکار منتظر پاسخ میماند. اما اگر همان کار را در یک coroutine انجام دهید، ترد به کمک event loop یک تسک دیگر را برای اجرا برمیدارد؛ در نتیجه تا وقتی پاسخ API آماده شود، ترد بهجای قفلشدن، مشغول اجرای کارهای دیگر است. این همان راز کارایی asyncio است.
تعریف coroutine
یک coroutine کاملاً شبیه تابع است، با دو تفاوت: با async def تعریف میشود و حداقل یک دستور await در بدنهاش دارد.
import asyncio
async def function_name():
await asyncio.sleep(1)برای شبیهسازی زمانبربودن یک عملیات از asyncio.sleep استفاده کردهایم. دقت کنید که نمیتوان از time.sleep استفاده کرد، چون time.sleep «awaitable» نیست و کل حلقه را بلاک میکند.
چطور میتوان یک coroutine را اجرا کرد؟
دقت کنید که یک coroutine را نمیتوان مثل یک تابع معمولی و با صداکردن مستقیم اجرا کرد:
import asyncio
async def download_file(file_name):
print(f'download {file_name} started')
await asyncio.sleep(2)
print(f'download {file_name} finished')
download_file('test.txt') # does NOT run; just creates a coroutine objectاجرای کد بالا این هشدار را میدهد و تابع اصلاً اجرا نمیشود:
RuntimeWarning: coroutine 'download_file' was never awaitedبرای اجرای یک coroutine سه روش داریم.
۱. asyncio.run
این روش یک coroutine را اجرا میکند. اما توجه کنید اگر آن را چند بار پشتسرهم صدا بزنید، coroutineها بهصورت ترتیبی (نه همزمان) اجرا میشوند:
import asyncio
async def download_file(file_name):
print(f'download {file_name} started')
await asyncio.sleep(2)
print(f'download {file_name} finished')
asyncio.run(download_file('test1.txt'))
asyncio.run(download_file('test2.txt'))خروجی نشان میدهد که کارها همزمان اجرا نشدهاند (کل کار حدود ۴ ثانیه طول میکشد):
download test1.txt started
download test1.txt finished
download test2.txt started
download test2.txt finished
این دقیقاً چیزی نیست که از asyncio میخواهیم، چون هدف ما اجرای همزمان درخواستهاست.
۲. asyncio.create_task + asyncio.run (روش پیشنهادی)
در این روش هر coroutine را داخل یک coroutine اصلی بهصورت یک Task اجرا میکنیم تا کارها واقعاً همزمان پیش بروند:
import asyncio
async def download_file(file_name):
print(f'download {file_name} started')
await asyncio.sleep(2)
print(f'download {file_name} finished')
async def manage_downloads():
t1 = asyncio.create_task(download_file('test1.txt'))
t2 = asyncio.create_task(download_file('test2.txt'))
await t1
await t2
asyncio.run(manage_downloads())این بار هر دو دانلود با هم شروع میشوند و کل کار حدود ۲ ثانیه (نه ۴ ثانیه) طول میکشد:
download test1.txt started
download test2.txt started
download test1.txt finished
download test2.txt finished
تفاوت کلیدی: create_task بلافاصله کار را در حلقه ثبت و شروع میکند؛ await فقط برای منتظرماندنِ نتیجه است، نه شروع کار. به همین دلیل هر دو Task پیش از اینکه منتظرشان بمانیم، همزمان در حال اجرا هستند.
۳. asyncio.new_event_loop + run_until_complete
در این روش خودمان event loop را میسازیم و اجرا میکنیم. این روش سطحپایینتر است و معمولاً فقط وقتی لازم میشود که به کنترل دقیق روی حلقه نیاز داشته باشیم:
import asyncio
async def download_file(file_name):
print(f'download {file_name} started')
await asyncio.sleep(2)
print(f'download {file_name} finished')
async def manage_downloads():
t1 = asyncio.create_task(download_file('test1.txt'))
t2 = asyncio.create_task(download_file('test2.txt'))
await t1
await t2
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(manage_downloads())
finally:
loop.close()مثال – دانلود همزمان چند فایل
در عمل، الگوی متداول این است که چند کار I/O را با create_task بسازیم و همه را با هم اجرا کنیم. مثال زیر دانلود پنج فایل را شبیهسازی میکند؛ بهجای اینکه ۵ برابرِ زمانِ یک دانلود طول بکشد، تقریباً بهاندازهی یک دانلود طول میکشد:
import asyncio
import time
async def download_file(file_name):
print(f'download {file_name} started')
await asyncio.sleep(2) # in real code: an async network request (e.g. aiohttp)
print(f'download {file_name} finished')
return file_name
async def main():
start = time.time()
files = [f'file-{i}.zip' for i in range(5)]
tasks = [asyncio.create_task(download_file(f)) for f in files]
results = await asyncio.gather(*tasks)
print("downloaded:", results, "in %.1fs" % (time.time() - start)) # ~2s, not ~10s
asyncio.run(main())در دنیای واقعی بهجای asyncio.sleep، یک درخواست شبکهی غیرمسدودکننده (مثلاً با aiohttp) قرار میگیرد؛ ساختار کد همین است.
جمعبندی
- coroutine با
async defتعریف میشود و میتواند وسط اجرا مکث کند و کنترل را به حلقه بدهد. - صداکردن مستقیم یک coroutine آن را اجرا نمیکند و فقط یک شیء coroutine میسازد (هشدار «never awaited»).
- سه روش اجرا:
asyncio.run(ساده/ترتیبی)،create_task+run(پیشنهادی و همزمان)، وnew_event_loop+run_until_complete(سطحپایین). - برای اجرای واقعیِ همزمان، کارها را با
create_taskبسازید و بعدawaitکنید (یا ازgatherاستفاده کنید).