@@ -488,6 +488,9 @@ The prior section said tasks store a list of callbacks, which wasn't entirely
488488accurate.
489489It's actually the ``Future `` class that implements this logic, which ``Task ``
490490inherits.
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
492495Futures may also be used directly (not via tasks).
493496Tasks 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
503506own 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.
539546If :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
541548will 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
543550will 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.
558568This is effectively the same as calling ``asyncio.sleep(0) ``, but this approach
559569offers 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+
591606Here 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+
617641But that's all for now. Hopefully you're ready to more confidently dive into
618642some async programming or check out advanced topics in the
619643:mod: `rest of the documentation <asyncio> `.
0 commit comments