]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-137026: Add an explainer guide for asyncio (GH-137215)
authorAlexander Nordin <alexander.f.nordin@gmail.com>
Sat, 9 Aug 2025 05:29:51 +0000 (22:29 -0700)
committerGitHub <noreply@github.com>
Sat, 9 Aug 2025 05:29:51 +0000 (22:29 -0700)
* - Add an explainer guide (aka HOWTO, not how-to) for asyncio.

* Fix linter errors.

* - Enforce max line length of roughly 79 chars.
- Start sentences on new lines to minimize disruption of diffs.

* Add reference to subinterpreters.

* - Significantly reduce article size. Remove both example sections & "Which concurrency do I want" section.

* Align section-header lengths with section names.

* - Remove reference to deleted section.

* - Fix a variety of rote style guide items like title-alignment, use of ie and $, and so forth.
- Add links to other parts of the docs for keywords and objects like await, coro, task, future, etc.

* - One last title alignment.

* - Style nit.

* - Rework a variety of I statements.

* Lint fix.

* - Firm up commentary on yield from in corotuines.

* Update language comparing await and yield from.

* - Remove await-ing Tasks and futures section

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

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* Update Doc/howto/a-conceptual-overview-of-asyncio.rst

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* - Address comments related to style & writing flow.

* per-thread event loop note.

* Add section describing coroutines roots in generators.

* Phrasing tweak.

* Use asyncio.create_task instead of asyncio.Task

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

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* Update Doc/howto/a-conceptual-overview-of-asyncio.rst

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* small phrasing.

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

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* phrasing nit.

* style nits

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

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* phrasing nit

* Fix misnaming of async generator.

* phrasing nits.

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

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* Update Doc/howto/a-conceptual-overview-of-asyncio.rst

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* consistent spacing

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

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* phrasing nits

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

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* Update Doc/howto/a-conceptual-overview-of-asyncio.rst

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* Update Doc/howto/a-conceptual-overview-of-asyncio.rst

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* Update Doc/howto/a-conceptual-overview-of-asyncio.rst

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* add conclusion

* nits

* - Variety of style & grammar improvements thanks to ZeroIntensity's comments.

* - Make all directives start with a 3 space indent. Then 4 thereafter.

* - Use :linenos: instead of manually writing the line numbers.

* - Fix label typo for article.

* fix label link.

* Apply suggestions from code review

Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* - introduce async-sleep name

* Phrasing

* nit

* ungendered octopus

* teammates

* jobs

* rework fella to penguin

* - remove byline; add seealso

* Change ref from asyncio to use seealso block.

* Remove typehints. Fix indentation in one code example.

* Slight rephrase for clarity.

* Make references point to asyncio. Wrap some long lines.

* - Variety of style/phrasing improvements based on PR feedback.

* phrasing.

* phrasing nit.

* Apply suggestions from code review

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* Update Doc/howto/a-conceptual-overview-of-asyncio.rst

Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
* nit

* Apply suggestions from code review

Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
* fix backticks.

* nits

* nit

* add section on asyncio.run

* title change under the hood.

* modify task coro example.

* howtos article link.

* prefer await without backticks.

* phrasing tweak.

* Rework phrasing around how await tasks pauses and returns control in the await section.

* move code block to beforfe explanation in coroutine under the hood.

* phrasing.

* link to yield from.

* style nits

* nit

* - Modify language re: event-loop cycling endlessly.
- Discuss why await was designed to not yield for coros.

* - Add a note about debug=True on asyncio.run to await coro section.

* clarity nit

* - Add two other references in seealso block.

* nit

* Language simplification

* Apply suggestions from code review

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
* nit

* grammar fix.

* fix

* worker bees

* rework event loop paragraph to significantly deemphasize queues

* remove all references to queue besides the initial analogy.

* add note about garbage collection of tasks

* add practical note re: garbage collection

* phrasing nits

* re arrange note on task gc.

* line wrap nit

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

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
* link to debug mode docs.

* readd part2 prefix.

* simplify title.

* fix titles. tihnk I messed this up earlier.

* avoid idiom in title.

* fix titles once agian.

* Apply suggestions from code review

Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
* rework task gc example.

* phrasing tweak.

* tewak.

* nit

* nit

* nit

* nit

---------

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <wk.cvs.github@sydorenko.org.ua>
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Doc/howto/a-conceptual-overview-of-asyncio.rst [new file with mode: 0644]
Doc/howto/index.rst
Doc/library/asyncio-future.rst
Doc/library/asyncio-task.rst
Doc/library/asyncio.rst

diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst
new file mode 100644 (file)
index 0000000..d68f7cc
--- /dev/null
@@ -0,0 +1,606 @@
+.. _a-conceptual-overview-of-asyncio:
+
+****************************************
+A Conceptual Overview of :mod:`!asyncio`
+****************************************
+
+This :ref:`HOWTO <how-tos>` article seeks to help you build a sturdy mental
+model of how :mod:`asyncio` fundamentally works, helping you understand the
+how and why behind the recommended patterns.
+
+You might be curious about some key :mod:`!asyncio` concepts.
+You'll be comfortably able to answer these questions by the end of this
+article:
+
+- What's happening behind the scenes when an object is awaited?
+- How does :mod:`!asyncio` differentiate between a task which doesn't need
+  CPU-time (such as a network request or file read) as opposed to a task that
+  does (such as computing n-factorial)?
+- How to write an asynchronous variant of an operation, such as
+  an async sleep or database request.
+
+.. seealso::
+
+   * The `guide <https://github.com/anordin95/a-conceptual-overview-of-asyncio/
+     tree/main>`_ that inspired this HOWTO article, by Alexander Nordin.
+   * This in-depth `YouTube tutorial series <https://www.youtube.com/
+     watch?v=Xbl7XjFYsN4&list=PLhNSoGM2ik6SIkVGXWBwerucXjgP1rHmB>`_ on
+     ``asyncio`` created by Python core team member, Łukasz Langa.
+   * `500 Lines or Less: A Web Crawler With asyncio Coroutines <https://
+     aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html>`_ by A.
+     Jesse Jiryu Davis and Guido van Rossum.
+
+--------------------------------------------
+A conceptual overview part 1: the high-level
+--------------------------------------------
+
+In part 1, we'll cover the main, high-level building blocks of :mod:`!asyncio`:
+the event loop, coroutine functions, coroutine objects, tasks and ``await``.
+
+==========
+Event Loop
+==========
+
+Everything in :mod:`!asyncio` happens relative to the event loop.
+It's the star of the show.
+It's like an orchestra conductor.
+It's behind the scenes managing resources.
+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 worker bees.
+
+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 :mod:`!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.
+
+Effective execution relies on jobs sharing well and cooperating; a greedy job
+could hog control and leave the other jobs to starve, rendering the overall
+event loop approach rather useless.
+
+::
+
+   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()
+
+=====================================
+Asynchronous functions and coroutines
+=====================================
+
+This is a basic, boring Python function::
+
+   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."
+       )
+
+Calling a regular function invokes its logic or body::
+
+   >>> 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.
+
+The :ref:`async def <async def>`, as opposed to just a plain ``def``, makes
+this an asynchronous function (or "coroutine function").
+Calling it creates and returns a :ref:`coroutine <coroutine>` object.
+
+::
+
+   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}."
+       )
+
+Calling the async function, ``loudmouth_penguin``, does not execute the print statement;
+instead, it creates a coroutine object::
+
+   >>> loudmouth_penguin(magic_number=3)
+   <coroutine object loudmouth_penguin at 0x104ed2740>
+
+The terms "coroutine function" and "coroutine object" are often conflated
+as coroutine.
+That can be confusing!
+In this article, coroutine specifically refers to a coroutine object, or more
+precisely, an instance of :data:`types.CoroutineType` (native coroutine).
+Note that coroutines can also exist as instances of
+:class:`collections.abc.Coroutine` -- a distinction that matters for type
+checking.
+
+A coroutine represents the function's body or logic.
+A coroutine has to be explicitly started; again, merely creating the coroutine
+does not start it.
+Notably, the coroutine can be paused and resumed at various points within the
+function's body.
+That pausing and resuming ability is what allows for asynchronous behavior!
+
+Coroutines and coroutine functions were built by leveraging the functionality
+of :term:`generators <generator iterator>` and
+:term:`generator functions <generator>`.
+Recall, a generator function is a function that :keyword:`yield`\s, like this
+one::
+
+   def get_random_number():
+       # This would be a bad random number generator!
+       print("Hi")
+       yield 1
+       print("Hello")
+       yield 7
+       print("Howdy")
+       yield 4
+       ...
+
+Similar to a coroutine function, calling a generator function does not run it.
+Instead, it creates a generator object::
+
+   >>> get_random_number()
+   <generator object get_random_number at 0x1048671c0>
+
+You can proceed to the next ``yield`` of a generator by using the
+built-in function :func:`next`.
+In other words, the generator runs, then pauses.
+For example::
+
+   >>> generator = get_random_number()
+   >>> next(generator)
+   Hi
+   1
+   >>> next(generator)
+   Hello
+   7
+
+=====
+Tasks
+=====
+
+Roughly speaking, :ref:`tasks <asyncio-task-obj>` 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 :keyword:`await`.
+The recommended way to create tasks is via :func:`asyncio.create_task`.
+
+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).
+
+Since there's only one event loop (in each thread), :mod:`!asyncio` takes care of
+associating the task with the event loop for you. As such, there's no need
+to specify the event loop.
+
+::
+
+   coroutine = loudmouth_penguin(magic_number=5)
+   # This creates a Task object and schedules its execution via the event loop.
+   task = asyncio.create_task(coroutine)
+
+Earlier, we manually created the event loop and set it to run forever.
+In practice, it's recommended to use (and common to see) :func:`asyncio.run`,
+which takes care of managing the event loop and ensuring the provided
+coroutine finishes before advancing.
+For example, many async programs follow this setup::
+
+   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!")
+
+It's important to be aware that the task itself is not added to the event loop,
+only a callback to the task is.
+This matters if the task object you created is garbage collected before it's
+called by the event loop.
+For example, consider this program:
+
+.. code-block::
+   :linenos:
+
+   async def hello():
+       print("hello!")
+
+   async def main():
+       asyncio.create_task(hello())
+       # Other asynchronous instructions which run for a while
+       # and cede control to the event loop...
+       ...
+
+   asyncio.run(main())
+
+Because there's no reference to the task object created on line 5, it *might*
+be garbage collected before the event loop invokes it.
+Later instructions in the coroutine ``main()`` hand control back to the event
+loop so it can invoke other jobs.
+When the event loop eventually tries to run the task, it might fail and
+discover the task object does not exist!
+This can also happen even if a coroutine keeps a reference to a task but
+completes before that task finishes.
+When the coroutine exits, local variables go out of scope and may be subject
+to garbage collection.
+In practice, ``asyncio`` and Python's garbage collector work pretty hard to
+ensure this sort of thing doesn't happen.
+But that's no reason to be reckless!
+
+=====
+await
+=====
+
+:keyword:`await` is a Python keyword that's commonly used in one of two
+different ways::
+
+   await task
+   await coroutine
+
+In a crucial way, the behavior of ``await`` depends on the type of object
+being awaited.
+
+Awaiting a task will cede control from the current task or coroutine to
+the event loop.
+In the process of relinquishing control, a few important things happen.
+We'll use the following code example to illustrate::
+
+   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.
+       ...
+
+In this example, imagine the event loop has passed control to the start of the
+coroutine ``plant_a_tree()``.
+As seen above, the coroutine creates a task and then awaits it.
+The ``await dig_the_hole_task`` instruction adds a callback (which will resume
+``plant_a_tree()``) to the ``dig_the_hole_task`` object's list of callbacks.
+And then, the instruction cedes control to the event loop.
+Some time later, the event loop will pass control to ``dig_the_hole_task``
+and the task will finish whatever it needs to do.
+Once the task finishes, it will add its various callbacks to the event loop,
+in this case, a call to resume ``plant_a_tree()``.
+
+Generally speaking, when the awaited task finishes (``dig_the_hole_task``),
+the original task or coroutine (``plant_a_tree()``) is added back to the event
+loops to-do list to be resumed.
+
+This is a basic, yet reliable mental model.
+In practice, the control handoffs are slightly more complex, but not by much.
+In part 2, we'll walk through the details that make this possible.
+
+**Unlike tasks, awaiting a coroutine does not hand control back to the event
+loop!**
+Wrapping a coroutine in a task first, then awaiting that would cede
+control.
+The behavior of ``await coroutine`` is effectively the same as invoking a
+regular, synchronous Python function.
+Consider this program::
+
+   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())
+
+The first statement in the coroutine ``main()`` creates ``task_b`` and schedules
+it for execution via the event loop.
+Then, ``coro_a()`` is repeatedly awaited. Control never cedes to the
+event loop which is why we see the output of all three ``coro_a()``
+invocations before ``coro_b()``'s output:
+
+.. code-block:: none
+
+   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...
+
+If we change ``await coro_a()`` to ``await asyncio.create_task(coro_a())``, the
+behavior changes.
+The coroutine ``main()`` cedes control to the event loop with that statement.
+The event loop then proceeds through its backlog of work, calling ``task_b``
+and then the task which wraps ``coro_a()`` before resuming the coroutine
+``main()``.
+
+.. code-block:: none
+
+   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!
+
+This behavior of ``await coroutine`` can trip a lot of people up!
+That example highlights how using only ``await coroutine`` could
+unintentionally hog control from other tasks and effectively stall the event
+loop.
+:func:`asyncio.run` can help you detect such occurences via the
+``debug=True`` flag which accordingly enables
+:ref:`debug mode <asyncio-debug-mode>`.
+Among other things, it will log any coroutines that monopolize execution for
+100ms or longer.
+
+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.
+That might sound minor, but in a large program with many ``await``'s and a deep
+callstack that overhead can add up to a meaningful performance drag.
+
+------------------------------------------------
+A conceptual overview part 2: the nuts and bolts
+------------------------------------------------
+
+Part 2 goes into detail on the mechanisms :mod:`!asyncio` uses to manage
+control flow.
+This is where the magic happens.
+You'll come away from this section knowing what ``await`` does behind the scenes
+and how to make your own asynchronous operators.
+
+================================
+The inner workings of coroutines
+================================
+
+:mod:`!asyncio` leverages four components to pass around control.
+
+:meth:`coroutine.send(arg) <generator.send>` is the method used to start or
+resume a coroutine.
+If the coroutine was paused and is now being resumed, the argument ``arg``
+will be sent in as the return value of the ``yield`` statement which originally
+paused it.
+If the coroutine is being used for the first time (as opposed to being resumed)
+``arg`` must be ``None``.
+
+.. code-block::
+   :linenos:
+
+   class Rock:
+       def __await__(self):
+           value_sent_in = yield 7
+           print(f"Rock.__await__ resuming with value: {value_sent_in}.")
+           return value_sent_in
+
+   async def main():
+       print("Beginning coroutine main().")
+       rock = Rock()
+       print("Awaiting rock...")
+       value_from_rock = await rock
+       print(f"Coroutine received value: {value_from_rock} from rock.")
+       return 23
+
+   coroutine = main()
+   intermediate_result = coroutine.send(None)
+   print(f"Coroutine paused and returned intermediate value: {intermediate_result}.")
+
+   print(f"Resuming coroutine and sending in value: 42.")
+   try:
+       coroutine.send(42)
+   except StopIteration as e:
+       returned_value = e.value
+   print(f"Coroutine main() finished and provided value: {returned_value}.")
+
+:ref:`yield <yieldexpr>`, like usual, pauses execution and returns control
+to the caller.
+In the example above, the ``yield``, on line 3, is called by
+``... = await rock`` on line 11.
+More broadly speaking, ``await`` calls the :meth:`~object.__await__` method of
+the given object.
+``await`` also does one more very special thing: it propagates (or "passes
+along") any ``yield``\ s it receives up the call-chain.
+In this case, that's back to ``... = coroutine.send(None)`` on line 16.
+
+The coroutine is resumed via the ``coroutine.send(42)`` call on line 21.
+The coroutine picks back up from where it ``yield``\ ed (or paused) on line 3
+and executes the remaining statements in its body.
+When a coroutine finishes, it raises a :exc:`StopIteration` exception with the
+return value attached in the :attr:`~StopIteration.value` attribute.
+
+That snippet produces this output:
+
+.. code-block:: none
+
+   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.
+
+It's worth pausing for a moment here and making sure you followed the various
+ways that control flow and values were passed. A lot of important ideas were
+covered and it's worth ensuring your understanding is firm.
+
+The only way to yield (or effectively cede control) from a coroutine is to
+``await`` an object that ``yield``\ s in its ``__await__`` method.
+That might sound odd to you. You might be thinking:
+
+   1. What about a ``yield`` directly within the coroutine function? The
+   coroutine function becomes an
+   :ref:`async generator function <asynchronous-generator-functions>`, a
+   different beast entirely.
+
+   2. What about a :ref:`yield from <yieldexpr>` 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.
+   Initially ``yield`` was barred as well, but was re-accepted to allow for
+   async generators.
+   Despite that, ``yield from`` and ``await`` effectively do the same thing.
+
+=======
+Futures
+=======
+
+A :ref:`future <asyncio-future-obj>` is an object meant to represent a
+computation's status and result.
+The term is a nod to the idea of something still to come or not yet happened,
+and the object is a way to keep an eye on that something.
+
+A future has a few important attributes. One is its state which can be either
+"pending", "cancelled" or "done".
+Another is its result, which is set when the state transitions to done.
+Unlike a coroutine, a future does not represent the actual computation to be
+done; instead, it represents the status and result of that computation, kind of
+like a status light (red, yellow or green) or indicator.
+
+:class:`asyncio.Task` subclasses :class:`asyncio.Future` in order to gain
+these various capabilities.
+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.
+
+Futures may also be used directly (not via tasks).
+Tasks mark themselves as done when their coroutine is complete.
+Futures are much more versatile and will be marked as done when you say so.
+In this way, they're the flexible interface for you to make your own conditions
+for waiting and resuming.
+
+========================
+A homemade asyncio.sleep
+========================
+
+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 a
+coroutine wrapped in a task: ``async_sleep(3)``.
+We want that task to finish only after three seconds have elapsed, but without
+preventing other tasks from running.
+
+::
+
+   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)
+
+
+Below, we use a future to enable custom control over when that task will be
+marked as done.
+If :meth:`future.set_result() <asyncio.Future.set_result>` (the method
+responsible for marking that future as done) is never called, then this task
+will never finish.
+We've 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()``.
+
+::
+
+   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
+
+Below, we'll use a rather bare object, ``YieldToEventLoop()``, to ``yield``
+from ``__await__`` in order to cede 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.
+Eventually, enough time will have elapsed, and ``_sleep_watcher(...)`` will
+mark the future as done, and then itself finish too by breaking out of the
+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 of true of ``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()
+
+Here is the full program's output:
+
+.. code-block:: none
+
+   $ 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.
+
+You might feel this implementation of asynchronous sleep was unnecessarily
+convoluted.
+And, well, it was.
+The example was meant to showcase the versatility of futures with a simple
+example that could be mimicked for more complex needs.
+For reference, you could implement it without futures, like so::
+
+   async def simpler_async_sleep(seconds):
+       time_to_wake = time.time() + seconds
+       while True:
+           if time.time() >= time_to_wake:
+               return
+           else:
+               await YieldToEventLoop()
+
+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 <asyncio>`.
index f350141004c2db4b8a0ec8a4ffe43be37133de5e..81fc7e63f35bd79168de1461301ba617808a3814 100644 (file)
@@ -1,3 +1,5 @@
+.. _how-tos:
+
 ***************
  Python HOWTOs
 ***************
@@ -11,6 +13,7 @@ Python Library Reference.
    :maxdepth: 1
    :hidden:
 
+   a-conceptual-overview-of-asyncio.rst
    cporting.rst
    curses.rst
    descriptor.rst
@@ -38,6 +41,7 @@ Python Library Reference.
 
 General:
 
+* :ref:`a-conceptual-overview-of-asyncio`
 * :ref:`annotations-howto`
 * :ref:`argparse-tutorial`
 * :ref:`descriptorhowto`
index 32771ba72e000267a57a3e59005e13e6aea32eee..4b69e569523c58b3f6a0ffaa6ae9ff6f1ae278d5 100644 (file)
@@ -75,6 +75,7 @@ Future Functions
       Deprecation warning is emitted if *future* is not a Future-like object
       and *loop* is not specified and there is no running event loop.
 
+.. _asyncio-future-obj:
 
 Future Object
 =============
index b19ffa8213a971316e6889bf9a9d769e861531c6..f825ae92ec74712b92a3fa385b68e0000f36d153 100644 (file)
@@ -1193,6 +1193,7 @@ Introspection
 
    .. versionadded:: 3.4
 
+.. _asyncio-task-obj:
 
 Task Object
 ===========
index 7d368dae49dc1d731f5b4d1dd77be2954a7864b2..444db01390d922d44b5bda01806e056163f2e1e3 100644 (file)
@@ -29,6 +29,11 @@ database connection libraries, distributed task queues, etc.
 asyncio is often a perfect fit for IO-bound and high-level
 **structured** network code.
 
+.. seealso::
+
+   :ref:`a-conceptual-overview-of-asyncio`
+      Explanation of the fundamentals of asyncio.
+
 asyncio provides a set of **high-level** APIs to:
 
 * :ref:`run Python coroutines <coroutine>` concurrently and