diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 3adfedbf410ecc..4d399c346c5405 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -488,6 +488,9 @@ The prior section said tasks store a list of callbacks, which wasn't entirely accurate. It's actually the ``Future`` class that implements this logic, which ``Task`` inherits. +You already saw how awaiting a Task relinquishes control back to the event loop, +this functionality is also inherited from :class:`!asyncio.Future`. +In other words, awaiting a Future will also give control back to the event loop. Futures may also be used directly (not via tasks). 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 own variant of asynchronous sleep (``async_sleep``) which mimics :func:`asyncio.sleep`. -This snippet registers a few tasks with the event loop and then awaits the task -created by ``asyncio.create_task``, which wraps the ``async_sleep(3)`` coroutine. -We want that task to finish only after three seconds have elapsed, but without -preventing other tasks from running. +This snippet registers a few tasks with the event loop and then awaits the +``async_sleep(3)`` coroutine. +We want that coroutine (which we'll see shortly) to finish only after three +seconds have elapsed, but without preventing other tasks from running. :: + def print_time(phrase: str): + print( + f"{phrase} at time: " + f"{datetime.datetime.now().strftime("%H:%M:%S")}." + ) + async def other_work(): print("I like work. Work work.") @@ -521,25 +530,23 @@ preventing other tasks from running. 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")}." - ) + + print_time("Beginning asynchronous sleep") + await async_sleep(3) + print_time("Done asynchronous sleep") + # asyncio.gather effectively awaits each task in the collection. await asyncio.gather(*work_tasks) +| -Below, we use a future to enable custom control over when that task will be -marked as done. +In the snippet below, showing the ``async_sleep`` coroutine function's +implementation, we use a future to enable custom control over when the +coroutine will finish *and* to cede control back to the event loop. If :meth:`future.set_result() ` (the method -responsible for marking that future as done) is never called, then this task +responsible for marking that future as done) is never called, then this coroutine will never finish. -We've also enlisted the help of another task, which we'll see in a moment, that +This snippet also enlisted the help of another task, which we'll see in a moment, that will monitor how much time has elapsed and, accordingly, call ``future.set_result()``. @@ -553,26 +560,15 @@ will monitor how much time has elapsed and, accordingly, call # Block until the future is marked as done. await future -Below, we use a rather bare ``YieldToEventLoop()`` object to ``yield`` -from its ``__await__`` method, ceding control to the event loop. +| + +Now, for the final snippet. +We use a rather bare ``YieldToEventLoop()`` object to ``yield`` +from its ``__await__`` method, thereby ceding control to the event loop. This is effectively the same as calling ``asyncio.sleep(0)``, but this approach offers more clarity, not to mention it's somewhat cheating to use ``asyncio.sleep`` when showcasing how to implement it! -As usual, the event loop cycles through its tasks, giving them control -and receiving control back when they pause or finish. -The ``watcher_task``, which runs the coroutine ``_sleep_watcher(...)``, will -be invoked once per full cycle of the event loop. -On each resumption, it'll check the time and if not enough has elapsed, then -it'll pause once again and hand control back to the event loop. -Once enough time has elapsed, ``_sleep_watcher(...)`` -marks the future as done and completes by exiting its -infinite ``while`` loop. -Given this helper task is only invoked once per cycle of the event loop, -you'd be correct to note that this asynchronous sleep will sleep *at least* -three seconds, rather than exactly three seconds. -Note this is also true of ``asyncio.sleep``. - :: class YieldToEventLoop: @@ -588,6 +584,26 @@ Note this is also true of ``asyncio.sleep``. else: await YieldToEventLoop() + +As usual, the event loop cycles through its jobs, giving them control +and receiving control back when they pause or finish. +The ``watcher_task``, which runs the coroutine ``_sleep_watcher(...)``, will +be invoked once per full cycle of the event loop. +On each resumption, it'll check the time and if not enough has elapsed, then +it'll pause once again and hand control back to the event loop. + +Once enough time has elapsed, ``_sleep_watcher(...)`` marks the future as +done and completes by exiting its infinite ``while`` loop. +Marking the future as done adds its list of callbacks, namely to resume the +``async_sleep(3)`` coroutine, to the event loop. +Some time later, the event loop resumes that coroutine. +Since there are no further instructions (the last one was ``await future``), +it too finishes and returns to ``main()`` where the program proceeds. +Given this helper task is only invoked once per cycle of the event loop, +you'd be correct to note that this asynchronous sleep will sleep *at least* +three seconds, rather than exactly three seconds. +Note this is also true of ``asyncio.sleep``. + Here is the full program's output: .. code-block:: none @@ -614,6 +630,15 @@ For reference, you could implement it without futures, like so:: else: await YieldToEventLoop() +.. note:: + + These examples use busy-waiting to simplify the implementation, but this + is not recommended in practice! Consider a case where there are no other + tasks in the event loop, besides ``_sleep_watcher()``. The program + will constantly pause and resume this task, effectively eating up + CPU resources for no good reason. To avoid this, you can put a short + synchronous (not asynchronous!) sleep in the else condition. + But that's all for now. Hopefully you're ready to more confidently dive into some async programming or check out advanced topics in the :mod:`rest of the documentation `.