در مثال درست کردن غذا در آشپزخانه، این شما هستید که مشخص میکنید ژله و سالاد و غذا چه زمانی درست شوند، در صورتی که در مثال امور روزمره، شما مشخص نمیکنید که بسته چه زمانی به دست شما برسد یا اینکه گیاهان چه زمانی رشد کنند و محصول دهند.
در multithreading ما (سیستمعامل) مشخص میکنیم که ترتیب و چگونگی انجام تسک ها به چه شکل باشد اما در asyncio خود تسک ها هستند که مشخص میکنند چه زمانی انجام خواهند شد. به عبارتی دیگر در asyncio ما تخمین درستی از زمانی که تسک به پایان خواهد رسید نداریم زیرا انجام شدن تسک به یک عامل خارجی (مثلا یک سرور دیگر) وابسته است.
coroutine چیست؟
از نظر ظاهری یک تابع است که برای تعریف آن از دستور async و در بدنه آن حتما حداقل یک await دارد. به تکه کد زیر توجه کنید.
import asyncio
async def function_name();
await asyncio.sleep(1)توضیح کد بالا:
- خط ۱: ماژول asyncio ایمپورت شده است تا بتوانیم از متد sleep آن استفاده کنیم.
- خط ۳: با استفاده از دستور
asyncیک coroutine تعریف کرده ایم. - خط ۴: با استفاده از دستور
awaitمشخص کرده ایم که یک پاسخ از یک عامل خارجی قرار است برگردد. با استفاده از دستورasyncio.sleepاینطور شبیه سازی کرده ایم که گویی قرار است پاسخ از عامل خارجی پس از ۱ ثانیه برگردد
همانطور که مشاهده میکنید در تکه کد بالا از asyncio.sleep استفاده شده است. ما نمیتوانیم از time.sleep استفاده کنیم زیرا متد sleep اش awaitable نیست.
توجه داشته باشید که دستور async و await ربطی به ماژول asyncio ندارند و توسط خود پایتون به صورت builtin قابل استفاده هستند.
اما از نظر مفهومی، coroutine یک تابع است که زمان خروجی دادنش به یک عامل خارجی بستگی دارد به صورتی که هر وقت آن عامل خارجی نتیجه مورد نظر را برگرداند، coroutine نیز با برگرداندن آن مقدار این را به ما اطلاع میدهد. این وابستگی ها به عامل خارجی با دستور await مشخص میشوند.
چطور میتوان coroutine را اجرا کرد؟
از آنجایی که تعریف آن شبیه به تابع است شاید فکر کنید که میتوان به صورت یک تابع به شکل زیر آن را فراخوانی کرد. به تکه کد زیر توجه کنید.
import asyncio
async def download_file(file_name):
print(f'dowload {file_name} started')
await asyncio.sleep(2)
print(f'dowload {file_name} finished')
download_file('test.txt')در صورتی که کد بالا را اجرا کنیم با warning زیر رو به رو خواهیم شد زیرا نباید یک coroutine را بدین شکل فراخوانی کرد.
RuntimeWarning: coroutine 'download_file' was never awaited
download_file('test.txt')
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
اجرای یک coroutine سه روش دارد:
- asyncio.run
- asyncio.create_task + asyncio.run (روش پیشنهادی)
- asyncio.new_event_loop + asyncio.run_until_complete
asyncio.run
یک coroutine را به صورت خطی اجرا میکند.
import asyncio
async def download_file(file_name):
print(f'dowload {file_name} started')
await asyncio.sleep(2)
print(f'dowload {file_name} finished')
asyncio.run(download_file('test1.txt'))
asyncio.run(download_file('test2.txt'))خروجی کد بالا میشود:
dowload test1.txt started
dowload test1.txt finished
dowload test2.txt started
dowload test2.txt finished
همانطور که مشخص است تسک ها به صورت همزمان اجرا نشده اند. قطعا این چیزی که ما از برنامهنویسی با asyncio میخواهیم نیست زیرا هدف ما اجرای همزمان درخواست هاست.
asyncio.create_task + asyncio.run
در این روش هر coroutine را در قالب یک task در یک coroutine اصلی اجرا خواهیم کرد.
import asyncio
async def download_file(file_name):
print(f'dowload {file_name} started')
await asyncio.sleep(2)
print(f'dowload {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())توضیح کد بالا:
- خط ۳ تا ۶: یک coroutine است که نام یک فایل را میگیرد و آن را دانلود میکند.
- خط ۸ تا ۱۲: یک coroutine است که coroutine های دیگر را اجرا میکند.
- خط ۱۴: در این خط coroutine اصلی اجرا میشود.
asyncio.new_event_loop + asyncio.run_until_complete
در این روش خودمان event loop را ایجاد و اجرا میکنیم.
import asyncio
async def download_file(file_name):
print(f'dowload {file_name} started')
await asyncio.sleep(2)
print(f'dowload {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()