mirror of
https://github.com/python/cpython.git
synced 2025-10-26 19:24:34 +00:00
* - 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>
606 lines
22 KiB
ReStructuredText
606 lines
22 KiB
ReStructuredText
.. _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>`.
|