Skip to content

Commit 3f6a8f0

Browse files
committed
Address issue-143727 regarding the homemade asyncio.sleep example in the asyncio HOWTO
1 parent 499706b commit 3f6a8f0

1 file changed

Lines changed: 57 additions & 33 deletions

File tree

Doc/howto/a-conceptual-overview-of-asyncio.rst

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,9 @@ The prior section said tasks store a list of callbacks, which wasn't entirely
488488
accurate.
489489
It's actually the ``Future`` class that implements this logic, which ``Task``
490490
inherits.
491+
You already saw how awaiting a Task relinquishes control back to the event loop,
492+
this functionality is also inherited from :class:`!asyncio.Future`.
493+
In other words, awaiting a Future will also give control back to the event loop.
491494

492495
Futures may also be used directly (not via tasks).
493496
Tasks mark themselves as done when their coroutine is complete.
@@ -503,13 +506,19 @@ We'll go through an example of how you could leverage a future to create your
503506
own variant of asynchronous sleep (``async_sleep``) which mimics
504507
:func:`asyncio.sleep`.
505508

506-
This snippet registers a few tasks with the event loop and then awaits the task
507-
created by ``asyncio.create_task``, which wraps the ``async_sleep(3)`` coroutine.
508-
We want that task to finish only after three seconds have elapsed, but without
509-
preventing other tasks from running.
509+
This snippet registers a few tasks with the event loop and then awaits the
510+
``async_sleep(3)`` coroutine.
511+
We want that coroutine (which we'll see shortly) to finish only after three
512+
seconds have elapsed, but without preventing other tasks from running.
510513

511514
::
512515

516+
def print_time(phrase: str):
517+
print(
518+
f"{phrase} at time: "
519+
f"{datetime.datetime.now().strftime("%H:%M:%S")}."
520+
)
521+
513522
async def other_work():
514523
print("I like work. Work work.")
515524

@@ -521,25 +530,23 @@ preventing other tasks from running.
521530
asyncio.create_task(other_work()),
522531
asyncio.create_task(other_work())
523532
]
524-
print(
525-
"Beginning asynchronous sleep at time: "
526-
f"{datetime.datetime.now().strftime("%H:%M:%S")}."
527-
)
528-
await asyncio.create_task(async_sleep(3))
529-
print(
530-
"Done asynchronous sleep at time: "
531-
f"{datetime.datetime.now().strftime("%H:%M:%S")}."
532-
)
533+
534+
print_time("Beginning asynchronous sleep")
535+
await async_sleep(3)
536+
print_time("Done asynchronous sleep")
537+
533538
# asyncio.gather effectively awaits each task in the collection.
534539
await asyncio.gather(*work_tasks)
535540

541+
|
536542
537-
Below, we use a future to enable custom control over when that task will be
538-
marked as done.
543+
In the snippet below, showing the ``async_sleep`` coroutine functions
544+
implementation, we use a future to enable custom control over when the
545+
coroutine will finish *and* to cede control back to the event loop.
539546
If :meth:`future.set_result() <asyncio.Future.set_result>` (the method
540-
responsible for marking that future as done) is never called, then this task
547+
responsible for marking that future as done) is never called, then this coroutine
541548
will never finish.
542-
We've also enlisted the help of another task, which we'll see in a moment, that
549+
This snippet also enlisted the help of another task, which we'll see in a moment, that
543550
will monitor how much time has elapsed and, accordingly, call
544551
``future.set_result()``.
545552

@@ -553,26 +560,15 @@ will monitor how much time has elapsed and, accordingly, call
553560
# Block until the future is marked as done.
554561
await future
555562

556-
Below, we use a rather bare ``YieldToEventLoop()`` object to ``yield``
557-
from its ``__await__`` method, ceding control to the event loop.
563+
|
564+
565+
Now, for the final snippet.
566+
We use a rather bare ``YieldToEventLoop()`` object to ``yield``
567+
from its ``__await__`` method, thereby ceding control to the event loop.
558568
This is effectively the same as calling ``asyncio.sleep(0)``, but this approach
559569
offers more clarity, not to mention it's somewhat cheating to use
560570
``asyncio.sleep`` when showcasing how to implement it!
561571

562-
As usual, the event loop cycles through its tasks, giving them control
563-
and receiving control back when they pause or finish.
564-
The ``watcher_task``, which runs the coroutine ``_sleep_watcher(...)``, will
565-
be invoked once per full cycle of the event loop.
566-
On each resumption, it'll check the time and if not enough has elapsed, then
567-
it'll pause once again and hand control back to the event loop.
568-
Once enough time has elapsed, ``_sleep_watcher(...)``
569-
marks the future as done and completes by exiting its
570-
infinite ``while`` loop.
571-
Given this helper task is only invoked once per cycle of the event loop,
572-
you'd be correct to note that this asynchronous sleep will sleep *at least*
573-
three seconds, rather than exactly three seconds.
574-
Note this is also true of ``asyncio.sleep``.
575-
576572
::
577573

578574
class YieldToEventLoop:
@@ -588,6 +584,25 @@ Note this is also true of ``asyncio.sleep``.
588584
else:
589585
await YieldToEventLoop()
590586

587+
588+
As usual, the event loop cycles through its jobs, giving them control
589+
and receiving control back when they pause or finish.
590+
The ``watcher_task``, which runs the coroutine ``_sleep_watcher(...)``, will
591+
be invoked once per full cycle of the event loop.
592+
On each resumption, it'll check the time and if not enough has elapsed, then
593+
it'll pause once again and hand control back to the event loop.
594+
595+
Once enough time has elapsed, ``_sleep_watcher(...)`` marks the future as
596+
done and completes by exiting its infinite ``while`` loop.
597+
In the process of marking the future as done, the future's list of callbacks,
598+
namely to resume the ``async_sleep(3)`` coroutine, are added to the event loop.
599+
Some time later, the event loop will resume that coroutine and the program will
600+
proceed in the ``main()`` coroutine.
601+
Given this helper task is only invoked once per cycle of the event loop,
602+
you'd be correct to note that this asynchronous sleep will sleep *at least*
603+
three seconds, rather than exactly three seconds.
604+
Note this is also true of ``asyncio.sleep``.
605+
591606
Here is the full program's output:
592607

593608
.. code-block:: none
@@ -614,6 +629,15 @@ For reference, you could implement it without futures, like so::
614629
else:
615630
await YieldToEventLoop()
616631

632+
.. note::
633+
634+
These examples use busy-waiting to simplify the implementation, but this
635+
is not recommended in practice! Consider a case where there are no other
636+
tasks in the event loop, besides ``_sleep_watcher()``. The program
637+
will constantly pause and resume this task, effectively eating up
638+
CPU resources for no good reason. To avoid this, you can put a short
639+
synchronous (not asynchronous!) sleep in the else condition.
640+
617641
But that's all for now. Hopefully you're ready to more confidently dive into
618642
some async programming or check out advanced topics in the
619643
:mod:`rest of the documentation <asyncio>`.

0 commit comments

Comments
 (0)