A conceptual overview of asyncio

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

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

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

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

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

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

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

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

Event loop

Everything in asyncio happens relative to the event loop. It's the star of the show, but prefers to work behind the scenes, managing and coordinating resources. It's like an orchestra conductor. Some power is explicitly granted to it, but a lot of its ability to get things done comes from the respect and cooperation of its band members.

In more technical terms, the event loop contains a collection of jobs to be run. Some jobs are added directly by you, and some indirectly by asyncio. The event loop takes a job from its backlog of work and invokes it (or "gives it control"), similar to calling a function, and then that job runs. Once it pauses or completes, it returns control to the event loop. The event loop will then select another job from its pool and invoke it. You can roughly think of the collection of jobs as a queue: jobs are added and then processed one at a time, generally (but not always) in order. This process repeats indefinitely, with the event loop cycling endlessly onwards. If there are no more jobs pending execution, the event loop is smart enough to rest and avoid needlessly wasting CPU cycles, and will come back when there's more work to be done, such as when I/O operations complete or timers expire.

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

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

وظایف

Roughly speaking, tasks are coroutines (not coroutine functions) tied to an event loop. A task also maintains a list of callback functions whose importance will become clear in a moment when we discuss await.

Creating a task automatically schedules it for execution (by adding a callback to run it in the event loop's to-do list, that is, collection of jobs). The recommended way to create tasks is via 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 می‌شود بستگی دارد.

Awaiting tasks

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()) دوباره به فهرست کارهای حلقه رویداد اضافه می‌شود تا از سر گرفته شود.

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

Awaiting coroutines

برخلاف وظایف، 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 تشخیص دهید، که حالت اشکال‌زدایی را فعال می‌کند. در میان سایر موارد، این حالت هر هم‌روالی را که اجرا را برای ۱۰۰ میلی‌ثانیه یا بیشتر در انحصار خود می‌گیرد، ثبت می‌کند.

The design intentionally trades off some conceptual clarity around usage of await for improved performance. Each time a task is awaited, control needs to be passed all the way up the call stack to the event loop. Then, the event loop needs to manage its internal state and work through its processing logic to resume the next job. That might sound minor, but in a large program with many awaits, that overhead can add up to a non-negligible performance drag.

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

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

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

asyncio leverages four components of Python to pass around control.

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 به‌طور مستقیم درون تابع هم‌روال باشد چطور؟ تابع هم‌روال به یک تابع تولیدگر ناهمگام تبدیل می‌شود، که به‌کلی مقوله‌ای متفاوت است.

2. What about a yield from within the coroutine function to a (plain) generator? That causes the error: SyntaxError: yield from not allowed in a coroutine. This was intentionally designed for the sake of simplicity -- mandating only one way of using coroutines. Despite that, yield from and await effectively do the same thing. Initially yield was barred as well, but was re-accepted to allow for async generators.

آینده‌نماها (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()

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