مروری مفهومی بر asyncio

این مقاله‌ی HOWTO در پی آن است که به شما کمک کند مدلی ذهنی مستحکم از اینکه asyncio در بنیاد چگونه کار می‌کند بسازید و چگونگی و چرایی پشت الگوهای توصیه‌شده را درک کنید.

ممکن است درباره‌ی برخی مفاهیم کلیدی asyncio کنجکاو باشید. در پایان این مقاله، می‌توانید به‌راحتی به این پرسش‌ها پاسخ دهید:

  • هنگامی که یک شیء await می‌شود، در پشت صحنه چه اتفاقی می‌افتد؟

  • asyncio چگونه بین وظیفه‌ایکه به زمان پردازنده نیاز ندارد (مانند درخواست شبکه یا خواندن پرونده) و وظیفه‌ای که به آن نیاز دارد (مانند محاسبه‌ی فاکتوریل n) تمایز قائل می‌شود؟

  • چگونگی نوشتن یک گونه‌ی ناهمگام از یک عملیات، مانند توقف ناهمگام یا درخواست پایگاه داده.

همچنین ملاحظه نمائید

مرور مفهومی بخش ۱: سطح بالا

در بخش ۱، اجزای سازنده‌ی اصلی و سطح بالای asyncio را پوشش خواهیم داد: حلقه‌ی رویداد، توابع هم‌روال، اشیاء هم‌روال، وظایف و await.

حلقه رویداد

همه‌چیز در asyncio نسبت به حلقه رویداد رخ می‌دهد. آن ستاره نمایش است. مانند رهبر ارکستر است. پشت صحنه منابع را مدیریت می‌کند. برخی قدرت‌ها به‌طور صریح به آن اعطا شده‌اند، اما بخش زیادی از توانایی آن برای انجام کارها از احترام و همکاری زنبورهای کارگرش ناشی می‌شود.

به بیان فنی‌تر، حلقه رویداد شامل مجموعه‌ای از کارها برای اجراست. برخی کارها مستقیماً توسط شما و برخی به‌صورت غیرمستقیم توسط asyncio اضافه می‌شوند. حلقه رویداد یک کار را از فهرست کارهای معوق خود می‌گیرد و آن را فراخوانی می‌کند (یا «کنترل را به آن می‌دهد»)، شبیه به فراخوانی یک تابع، و سپس آن کار اجرا می‌شود. هنگامی که آن کار مکث کند یا به پایان برسد، کنترل را به حلقه رویداد بازمی‌گرداند. سپس حلقه رویداد کار دیگری را از استخر خود انتخاب می‌کند و آن را فراخوانی می‌کند. شما تقریباً می‌توانید مجموعه کارها را یک صف در نظر بگیرید: کارها اضافه می‌شوند و سپس یکی‌یکی پردازش می‌شوند، معمولاً (اما نه همیشه) به‌ترتیب. این فرآیند به‌طور نامحدود تکرار می‌شود، در حالی که حلقه رویداد بی‌پایان به چرخه خود ادامه می‌دهد. اگر دیگر کاری در انتظار اجرا نباشد، حلقه رویداد به‌اندازه کافی هوشمند است که استراحت کند و از هدر دادن بیهوده چرخه‌های CPU جلوگیری کند، و هنگامی که کار بیشتری برای انجام وجود داشته باشد بازمی‌گردد.

اجرای کارآمد به اشتراک‌گذاری و همکاری مناسب کارها وابسته است؛ یک کار حریص می‌تواند کنترل را تصاحب کند و سایر کارها را محروم نگه دارد، و در نتیجه رویکرد کلی حلقه رویداد را عملاً بی‌فایده می‌سازد.

import asyncio

# This creates an event loop and indefinitely cycles through
# its collection of jobs.
event_loop = asyncio.new_event_loop()
event_loop.run_forever()

توابع ناهمگام و هم‌روال‌ها

این یک تابع ساده و خسته‌کننده در پایتون است:

def hello_printer():
    print(
        "Hi, I am a lowly, simple printer, though I have all I "
        "need in life -- \nfresh paper and my dearly beloved octopus "
        "partner in crime."
    )

فراخوانی یک تابع معمولی، منطق یا بدنه‌ی آن را اجرا می‌کند:

>>> hello_printer()
Hi, I am a lowly, simple printer, though I have all I need in life --
fresh paper and my dearly beloved octopus partner in crime.

async def، برخلاف تنها یک def ساده، این را به یک تابع ناهمگام (یا «تابع هم‌روال») تبدیل می‌کند. فراخوانی آن یک شیء هم‌روال را ایجاد می‌کند و بازمی‌گرداند.

async def loudmouth_penguin(magic_number: int):
    print(
     "I am a super special talking penguin. Far cooler than that printer. "
     f"By the way, my lucky number is: {magic_number}."
    )

فراخوانی تابع ناهمگام loudmouth_penguin، دستور print را اجرا نمی‌کند؛ در عوض، یک شیء هم‌روال ایجاد می‌کند:

>>> loudmouth_penguin(magic_number=3)
<coroutine object loudmouth_penguin at 0x104ed2740>

اصطلاح‌های «تابع هم‌روال» و «شیء هم‌روال» اغلب به‌عنوان هم‌روال با هم خلط می‌شوند. این می‌تواند گیج‌کننده باشد! در این مقاله، هم‌روال به‌طور مشخص به یک شیء هم‌روال اشاره دارد، یا دقیق‌تر، به یک نمونه از types.CoroutineType (هم‌روال بومی). توجه داشته باشید که هم‌روال‌ها می‌توانند به‌عنوان نمونه‌هایی از collections.abc.Coroutine نیز وجود داشته باشند — تمایزی که برای بررسی نوع اهمیت دارد.

یک هم‌روال نمایانگر بدنه یا منطق تابع است. یک هم‌روال باید به‌صراحت آغاز شود؛ باز هم، صرفاً ایجاد کردن هم‌روال، آن را آغاز نمی‌کند. به‌ویژه، هم‌روال می‌تواند در نقاط مختلفی از بدنه تابع متوقف و از سر گرفته شود. همین توانایی توقف و از سرگیری است که امکان رفتار ناهمگام را فراهم می‌کند!

هم‌روال‌ها و توابع هم‌روال با بهره‌گیری از قابلیت تولیدگرها و توابع تولیدگر ساخته شده‌اند. به یاد داشته باشید که تابع تولیدگر، تابعی است که yield می‌کند، مانند این تابع:

def get_random_number():
    # This would be a bad random number generator!
    print("Hi")
    yield 1
    print("Hello")
    yield 7
    print("Howdy")
    yield 4
    ...

همانند یک تابع هم‌روال، فراخوانی یک تابع تولیدگر آن را اجرا نمی‌کند. در عوض، یک شیء تولیدگر ایجاد می‌کند:

>>> get_random_number()
<generator object get_random_number at 0x1048671c0>

می‌توانید با استفاده از تابع توکار next() به yield بعدی یک تولیدگر بروید. به عبارت دیگر، تولیدگر اجرا می‌شود، سپس مکث می‌کند. برای مثال:

>>> generator = get_random_number()
>>> next(generator)
Hi
1
>>> next(generator)
Hello
7

وظایف

به‌طور تقریبی، وظایف هم‌روال‌هایی (نه توابع هم‌روال) هستند که به یک حلقه رویداد وابسته‌اند. یک وظیفه نیز فهرستی از توابع کال‌بک را نگه‌داری می‌کند که اهمیت آن‌ها لحظه‌ای دیگر، هنگامی که درباره await بحث می‌کنیم، روشن خواهد شد. روش توصیه‌شده برای ایجاد وظایف، استفاده از asyncio.create_task() است.

ایجاد یک وظیفهبه‌طور خودکار آن را برای اجرا زمان‌بندی می‌کند (با افزودن یک کال‌بک برای اجرای آن در فهرست کارهای حلقه رویداد، یعنی مجموعه‌ای از کارها).

asyncio به‌طور خودکار وظایف را برای شما به حلقه رویداد مرتبط می‌کند. این ارتباط خودکار عمداً برای سادگی در asyncio طراحی شده است. بدون آن، شما باید شیء حلقه رویداد را پیگیری کنید و آن را به هر تابع هم‌روالی که بخواهد وظایف را ایجاد کند، بفرستید، که این کار باعث ایجاد شلوغی اضافی در کد شما می‌شود.

coroutine = loudmouth_penguin(magic_number=5)
# This creates a Task object and schedules its execution via the event loop.
task = asyncio.create_task(coroutine)

پیش‌تر، حلقه رویداد را به‌صورت دستی ایجاد کردیم و آن را تنظیم کردیم تا برای همیشه اجرا شود. در عمل، توصیه می‌شود (و رایج است) از asyncio.run() استفاده کنید، که مدیریت حلقه رویداد را بر عهده می‌گیرد و تضمین می‌کند که هم‌روال ارائه‌شده پیش از ادامه به پایان برسد. برای مثال، بسیاری از برنامه‌های ناهمگام از این پیکربندی پیروی می‌کنند:

import asyncio

async def main():
    # Perform all sorts of wacky, wild asynchronous things...
    ...

if __name__ == "__main__":
    asyncio.run(main())
    # The program will not reach the following print statement until the
    # coroutine main() finishes.
    print("coroutine main() is done!")

مهم است بدانید که خود وظیفهبه حلقه رویداد اضافه نمی‌شود، بلکه تنها یک کال‌بک به آن اضافه می‌شود. اگر شیء وظیفه‌ای که شما ایجاد کرده‌اید پیش از آنکه توسط حلقه رویداد فراخوانی شود زباله‌روبی شود، این موضوع اهمیت دارد. برای مثال، این برنامه را در نظر بگیرید:

 1async def hello():
 2    print("hello!")
 3
 4async def main():
 5    asyncio.create_task(hello())
 6    # Other asynchronous instructions which run for a while
 7    # and cede control to the event loop...
 8    ...
 9
10asyncio.run(main())

از آنجا که هیچ ارجاعی به شیء وظیفه‌ی ایجادشده در خط ۵ وجود ندارد، ممکن است پیش از آنکه حلقه رویداد آن را فراخوانی کند، زباله‌روبی شود. دستورهای بعدی در هم‌روال main() کنترل را به حلقه رویداد بازمی‌گردانند تا بتواند کارهای دیگر را فراخوانی کند. وقتی حلقه رویداد سرانجام تلاش می‌کند تا وظیفه را اجرا کند، ممکن است با شکست مواجه شود و متوجه شود که شیء وظیفه وجود ندارد! این اتفاق نیز ممکن است حتی در صورتی رخ دهد که یک هم‌روال ارجاعی به یک وظیفه نگه دارد، اما پیش از آنکه آن وظیفه تمام شود، به پایان برسد. هنگامی که هم‌روال خارج می‌شود، متغیرهای محلی از محدوده خارج می‌شوند و ممکن است مشمول زباله‌روبی شوند. در عمل، asyncio و زباله‌روی پایتون تلاش زیادی می‌کنند تا اطمینان حاصل کنند که این قبیل موارد رخ نمی‌دهد. اما این دلیلی برای بی‌احتیاطی نیست!

await

await یک کلیدواژه پایتون است که معمولاً به یکی از دو روش متفاوت استفاده می‌شود:

await task
await coroutine

به‌شکلی اساسی، رفتار await به نوع شیء‌ای که await می‌شود بستگی دارد.

await کردن یک task، کنترل را از task یا هم‌روال فعلی به حلقه رویداد واگذار می‌کند. در فرآیند واگذاری کنترل، چند اتفاق مهم رخ می‌دهد. برای نشان دادن این موضوع از مثال کد زیر استفاده می‌کنیم:

async def plant_a_tree():
    dig_the_hole_task = asyncio.create_task(dig_the_hole())
    await dig_the_hole_task

    # Other instructions associated with planting a tree.
    ...

در این مثال، تصور کنید حلقه رویداد کنترل را به آغاز هم‌روال plant_a_tree() واگذار کرده است. همان‌طور که در بالا دیده شد، این هم‌روال یک وظیفهایجاد می‌کند و سپس آن را await می‌کند. دستور await dig_the_hole_task یک کال‌بک (که plant_a_tree() را از سر خواهد گرفت) به فهرست کال‌بک‌های شیء dig_the_hole_task اضافه می‌کند. و سپس، این دستور کنترل را به حلقه رویداد واگذار می‌کند. چندی بعد، حلقه رویداد کنترل را به dig_the_hole_task واگذار خواهد کرد و وظیفه هر کاری را که لازم است انجام دهد به پایان خواهد رساند. پس از پایان وظیفه، وظیفه کال‌بک‌های مختلف خود را به حلقه رویداد اضافه خواهد کرد که در این مورد، یک فراخوانی برای از سرگیری plant_a_tree() است.

به‌طور کلی، هنگامی که وظیفه awaitشده (dig_the_hole_task) تمام می‌شود، وظیفه یا هم‌روال اصلی (plant_a_tree()) دوباره به فهرست کارهای حلقه رویداد اضافه می‌شود تا از سر گرفته شود.

این یک مدل ذهنی پایه‌ای، اما قابل‌اتکا است. در عمل، انتقال‌های کنترل کمی پیچیده‌تر هستند، اما نه چندان زیاد. در بخش ۲، جزئیاتی را که این امر را ممکن می‌سازند، بررسی خواهیم کرد.

برخلاف وظایف، await کردن یک هم‌روال کنترل را به حلقه رویداد بازنمی‌گرداند! قرار دادن یک هم‌روال در یک وظیفه و سپس await کردن آن، کنترل را واگذار می‌کند. رفتار await coroutine عملاً همانند فراخوانی یک تابع پایتون عادی و همگام است. این برنامه را در نظر بگیرید:

import asyncio

async def coro_a():
   print("I am coro_a(). Hi!")

async def coro_b():
   print("I am coro_b(). I sure hope no one hogs the event loop...")

async def main():
   task_b = asyncio.create_task(coro_b())
   num_repeats = 3
   for _ in range(num_repeats):
      await coro_a()
   await task_b

asyncio.run(main())

اولین دستور در هم‌روال main()، task_b را ایجاد می‌کند و آن را برای اجرا از طریق حلقه رویداد زمان‌بندی می‌کند. سپس، coro_a() به‌طور مکرر await می‌شود. کنترل هرگز به حلقه رویداد واگذار نمی‌شود، به همین دلیل خروجی هر سه فراخوانی coro_a() را پیش از خروجی coro_b() می‌بینیم:

I am coro_a(). Hi!
I am coro_a(). Hi!
I am coro_a(). Hi!
I am coro_b(). I sure hope no one hogs the event loop...

اگر await coro_a() را به await asyncio.create_task(coro_a()) تغییر دهیم، رفتار تغییر می‌کند. هم‌روال main() با آن دستور، کنترل را به حلقه رویداد واگذار می‌کند. سپس حلقه رویداد کارهای معوق خود را پیش می‌برد، task_b و سپس وظیفهی را که coro_a() را می‌پوشاند فراخوانی می‌کند، پیش از آنکه هم‌روال main() را از سر بگیرد.

I am coro_b(). I sure hope no one hogs the event loop...
I am coro_a(). Hi!
I am coro_a(). Hi!
I am coro_a(). Hi!

این رفتار await coroutine می‌تواند بسیاری از افراد را دچار اشتباه کند! آن مثال نشان می‌دهد که چگونه استفاده‌ی صرف از await coroutine می‌تواند به‌طور غیرعمدی کنترل را از سایر وظایف تصاحب کند و عملاً حلقه رویداد را متوقف کند. asyncio.run() می‌تواند به شما کمک کند تا چنین مواردی را از طریق پرچم debug=True تشخیص دهید، که حالت اشکال‌زدایی را فعال می‌کند. در میان سایر موارد، این حالت هر هم‌روالی را که اجرا را برای ۱۰۰ میلی‌ثانیه یا بیشتر در انحصار خود می‌گیرد، ثبت می‌کند.

طراحی عمداً تا حدی وضوح مفهومی پیرامون استفاده از await را با بهبود عملکرد معاوضه می‌کند. هر بار که یک وظیفه در انتظار قرار می‌گیرد، کنترل باید از کل پشته فراخوانی به حلقه رویداد منتقل شود. ممکن است این موضوع جزئی به نظر برسد، اما در یک برنامه بزرگ با تعداد زیادی دستور await و پشته فراخوانی عمیق، این سربار می‌تواند به افت عملکرد قابل‌توجهی منجر شود.

مرور کلی مفهومی، بخش ۲: جزئیات عملی

بخش ۲ به تفصیل به سازوکارهایی می‌پردازد که asyncio برای مدیریت جریان کنترل به کار می‌برد. اینجاست که جادو رخ می‌دهد. پس از این بخش، خواهید دانست که await در پشت صحنه چه کاری انجام می‌دهد و چگونه عملگرهای ناهمگام خودتان را بسازید.

سازوکار درونی هم‌روال‌ها

asyncio برای جابه‌جایی کنترل از چهار کامپوننت بهره می‌گیرد.

coroutine.send(arg) متدی است که برای آغاز یا از سر گرفتن یک هم‌روال به کار می‌رود. اگر هم‌روال متوقف شده باشد و اکنون از سر گرفته شود، آرگومان arg به‌عنوان مقدار بازگشتی دستور yield که در ابتدا آن را متوقف کرده بود، فرستاده می‌شود. اگر هم‌روال برای اولین بار استفاده شود (نه اینکه از سر گرفته شود)، arg باید None باشد.

 1class Rock:
 2    def __await__(self):
 3        value_sent_in = yield 7
 4        print(f"Rock.__await__ resuming with value: {value_sent_in}.")
 5        return value_sent_in
 6
 7async def main():
 8    print("Beginning coroutine main().")
 9    rock = Rock()
10    print("Awaiting rock...")
11    value_from_rock = await rock
12    print(f"Coroutine received value: {value_from_rock} from rock.")
13    return 23
14
15coroutine = main()
16intermediate_result = coroutine.send(None)
17print(f"Coroutine paused and returned intermediate value: {intermediate_result}.")
18
19print(f"Resuming coroutine and sending in value: 42.")
20try:
21    coroutine.send(42)
22except StopIteration as e:
23    returned_value = e.value
24print(f"Coroutine main() finished and provided value: {returned_value}.")

yield، مانند همیشه، اجرا را متوقف می‌کند و کنترل را به فراخوان بازمی‌گرداند. در مثال بالا، yield در خط ۳ توسط ... = await rock در خط ۱۱ فراخوانی می‌شود. به‌طور کلی‌تر، await متد __await__() شیء داده‌شده را فراخوانی می‌کند. await یک کار بسیار خاص دیگر نیز انجام می‌دهد: هر yields را که دریافت می‌کند، در زنجیره‌ی فراخوانی به بالا منتشر (یا «منتقل») می‌کند. در این حالت، این مورد به ... = coroutine.send(None) در خط ۱۶ بازمی‌گردد.

هم‌روال از طریق فراخوانی coroutine.send(42) در خط ۲۱ از سر گرفته می‌شود. هم‌روال از همان جایی که در خط ۳ yield داده بود (یا مکث کرده بود) ادامه می‌یابد و دستورهای باقی‌مانده در بدنه‌ی خود را اجرا می‌کند. هنگامی که یک هم‌روال به پایان می‌رسد، استثنای StopIteration را همراه با مقدار بازگشتی پیوست‌شده در ویژگی value پرتاب می‌کند.

آن قطعه‌کد این خروجی را تولید می‌کند:

Beginning coroutine main().
Awaiting rock...
Coroutine paused and returned intermediate value: 7.
Resuming coroutine and sending in value: 42.
Rock.__await__ resuming with value: 42.
Coroutine received value: 42 from rock.
Coroutine main() finished and provided value: 23.

ارزش دارد که در اینجا لحظه‌ای درنگ کنید و اطمینان حاصل کنید که روش‌های مختلفی را که جریان کنترل و مقادیر از طریق آن‌ها منتقل می‌شدند، دنبال کرده‌اید. ایده‌های مهم بسیاری پوشش داده شد و ارزش دارد که اطمینان حاصل کنید درک شما استوار است.

تنها راه برای yield کردن از یک هم‌روال (یا عملاً واگذار کردن کنترل) این است که شیئی را await کنید که در متد __await__ خود yield می‌کند. ممکن است این موضوع برای شما عجیب به نظر برسد. شاید فکر کنید:

۱. اگر یک yield به‌طور مستقیم درون تابع هم‌روال باشد چطور؟ تابع هم‌روال به یک تابع تولیدگر ناهمگام تبدیل می‌شود، که به‌کلی مقوله‌ای متفاوت است.

۲. اگر درون تابع هم‌روال، یک yield from به یک تولیدگر (ساده) داشته باشید، چه؟ این باعث خطای زیر می‌شود: SyntaxError: yield from not allowed in a coroutine. این به‌عمد به‌خاطر سادگی طراحی شده است — تا تنها یک روش برای استفاده از هم‌روال‌ها الزامی باشد. در ابتدا yield نیز ممنوع بود، اما برای امکان‌پذیر شدن تولیدگرهای ناهمگام دوباره پذیرفته شد. با وجود این، yield from و await عملاً کار یکسانی انجام می‌دهند.

آینده‌نماها (Futures)

یک future شیئی است که برای نمایش وضعیت و نتیجه‌ی یک محاسبه در نظر گرفته شده است. این اصطلاح اشاره‌ای به مفهوم چیزی است که هنوز در راه است یا هنوز رخ نداده است، و این شیء راهی برای نظارت بر آن چیز است.

یک آینده (future) چند ویژگی مهم دارد. یکی از آن‌ها وضعیت آن است، که می‌تواند "pending"، "cancelled" یا "done" باشد. دیگری نتیجه آن است، که با تغییر وضعیت به done تنظیم می‌شود. برخلاف یک هم‌روال، آینده بیانگر محاسبه واقعی که قرار است انجام شود نیست؛ در عوض، وضعیت و نتیجه آن محاسبه را نشان می‌دهد، تقریباً مانند چراغ وضعیت (قرمز، زرد یا سبز) یا نشانگر.

asyncio.Task زیرکلاسی از asyncio.Future است تا این قابلیت‌های گوناگون را کسب کند. در بخش پیشین گفته شد که Taskها فهرستی از کال‌بک‌ها را ذخیره می‌کنند، که کاملاً دقیق نبود. در واقع این کلاس Future است که این منطق را پیاده‌سازی می‌کند و Task آن را به ارث می‌برد.

همچنین می‌توان از آینده‌نماها (Futures) به‌صورت مستقیم (نه از طریق وظایف ) استفاده کرد. وظایف خود را هنگامی که هم‌روال آن‌ها به پایان برسد، به‌عنوان انجام‌شده علامت‌گذاری می‌کنند. آینده‌نماها بسیار چندمنظوره‌تر هستند و هنگامی که شما تعیین کنید، به‌عنوان انجام‌شده علامت‌گذاری می‌شوند. به این ترتیب، آن‌ها رابطی انعطاف‌پذیر هستند که به شما امکان می‌دهند شرایط خودتان را برای انتظار و ازسرگیری ایجاد کنید.

یک asyncio.sleep دست‌ساز

در مثالی بررسی می‌کنیم که چگونه می‌توانید از یک آینده برای ایجاد نسخه‌ی اختصاصی خودتان از توقف ناهمگام (async_sleep) که همانند asyncio.sleep() رفتار می‌کند، بهره بگیرید.

این قطعه‌کد چند وظیفه را در حلقه‌ی رویداد ثبت می‌کند و سپس وظیفه‌ی ایجادشده توسط asyncio.create_task را await می‌کند، که هم‌روال async_sleep(3) را در بر می‌گیرد. ما می‌خواهیم آن وظیفه تنها پس از گذشت سه ثانیه به پایان برسد، اما بدون این‌که مانع اجرای سایر وظایف شود.

async def other_work():
    print("I like work. Work work.")

async def main():
    # Add a few other tasks to the event loop, so there's something
    # to do while asynchronously sleeping.
    work_tasks = [
        asyncio.create_task(other_work()),
        asyncio.create_task(other_work()),
        asyncio.create_task(other_work())
    ]
    print(
        "Beginning asynchronous sleep at time: "
        f"{datetime.datetime.now().strftime("%H:%M:%S")}."
    )
    await asyncio.create_task(async_sleep(3))
    print(
        "Done asynchronous sleep at time: "
        f"{datetime.datetime.now().strftime("%H:%M:%S")}."
    )
    # asyncio.gather effectively awaits each task in the collection.
    await asyncio.gather(*work_tasks)

در زیر، از یک فیوچر استفاده می‌کنیم تا امکان کنترل سفارشی بر زمان علامت‌گذاری آن وظیفهبه‌عنوان انجام‌شده فراهم شود. اگر future.set_result() (متد مسئول علامت‌گذاری آن فیوچر به‌عنوان انجام‌شده) هرگز فراخوانی نشود، این وظیفه هرگز به پایان نخواهد رسید. ما همچنین از کمک وظیفه دیگری بهره گرفته‌ایم که اندکی بعد آن را خواهیم دید؛ این وظیفه میزان زمان سپری‌شده را پایش خواهد کرد و بر همین اساس، future.set_result() را فراخوانی خواهد کرد.

async def async_sleep(seconds: float):
    future = asyncio.Future()
    time_to_wake = time.time() + seconds
    # Add the watcher-task to the event loop.
    watcher_task = asyncio.create_task(_sleep_watcher(future, time_to_wake))
    # Block until the future is marked as done.
    await future

در زیر، از یک شیء نسبتاً ساده‌ی YieldToEventLoop() استفاده می‌کنیم تا از متد __await__ آن yield کنیم و کنترل را به حلقه‌ی رویداد واگذار کنیم. این عملاً همان فراخوانی asyncio.sleep(0) است، اما این رویکرد وضوح بیشتری دارد؛ جدا از این‌که استفاده از asyncio.sleep هنگام نمایش چگونگی پیاده‌سازی آن تا حدی تقلب محسوب می‌شود!

مانند همیشه، حلقه رویداد میان وظایف خود می‌چرخد، کنترل را به آن‌ها می‌دهد و هنگامی که مکث می‌کنند یا به پایان می‌رسند، کنترل را پس می‌گیرد. watcher_task، که هم‌روال _sleep_watcher(...) را اجرا می‌کند، یک‌بار در هر چرخه‌ی کامل حلقه رویداد فراخوانی می‌شود. در هر بار ازسرگیری، زمان را بررسی می‌کند و اگر به اندازه کافی زمان سپری نشده باشد، دوباره مکث می‌کند و کنترل را به حلقه رویداد بازمی‌گرداند. هنگامی که به اندازه کافی زمان سپری شد، _sleep_watcher(...) آینده را به‌عنوان انجام‌شده علامت‌گذاری می‌کند و با خروج از حلقه‌ی while بی‌پایان خود به پایان می‌رسد. با توجه به این‌که این وظیفه کمکی تنها یک‌بار در هر چرخه‌ی حلقه رویداد فراخوانی می‌شود، درست است که بگویید این توقف ناهمگام حداقل ۳ ثانیه طول می‌کشد، نه دقیقاً ۳ ثانیه. توجه داشته باشید که این موضوع درباره‌ی asyncio.sleep نیز صادق است.

class YieldToEventLoop:
    def __await__(self):
        yield

async def _sleep_watcher(future, time_to_wake):
    while True:
        if time.time() >= time_to_wake:
            # This marks the future as done.
            future.set_result(None)
            break
        else:
            await YieldToEventLoop()

در اینجا خروجی کامل برنامه آمده است:

$ python custom-async-sleep.py
Beginning asynchronous sleep at time: 14:52:22.
I like work. Work work.
I like work. Work work.
I like work. Work work.
Done asynchronous sleep at time: 14:52:25.

ممکن است احساس کنید این پیاده‌سازی توقف ناهمگام بی‌مورد پیچیده بوده است. و خب، همین‌طور بود. این مثال برای نشان دادن تطبیق‌پذیری آینده‌نماها (futures) با مثالی ساده در نظر گرفته شده بود که می‌توان از آن برای نیازهای پیچیده‌تر الگو گرفت. برای مرجع، می‌توانید آن را بدون آینده‌نماها (futures) پیاده‌سازی کنید، به این صورت:

async def simpler_async_sleep(seconds):
    time_to_wake = time.time() + seconds
    while True:
        if time.time() >= time_to_wake:
            return
        else:
            await YieldToEventLoop()

اما برای اکنون، همین کافی است. امیدواریم شما آماده باشید تا با اطمینان بیشتری وارد برنامه‌نویسی ناهمگام شوید یا موضوعات پیشرفته را در بقیه مستندات مرور کنید.