Skip to content

Commit 8bf6d2c

Browse files
committed
- 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.
1 parent 3852bb1 commit 8bf6d2c

File tree

3 files changed

+82
-75
lines changed

3 files changed

+82
-75
lines changed

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

Lines changed: 80 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
.. _a-conceputal-overview-of-asyncio:
22

3-
*********************************
3+
********************************
44
A Conceptual Overview of asyncio
5-
*********************************
5+
********************************
66

77
:Author: Alexander Nordin
88

@@ -16,7 +16,7 @@ curiosity (read: drove me nuts).
1616
You should be able to comfortably answer all these questions by the end
1717
of this article.
1818

19-
- What's roughly happening behind the scenes when an object is ``await``-ed?
19+
- What's roughly happening behind the scenes when an object is ``await``\ ed?
2020
- How does asyncio differentiate between a task which doesn't need CPU-time
2121
to make progress towards completion, for example, a network request or file
2222
read as opposed to a task that does need cpu-time to make progress, like
@@ -29,60 +29,60 @@ A conceptual overview part 1: the high-level
2929
---------------------------------------------
3030

3131
In part 1, we'll cover the main, high-level building blocks of asyncio: the
32-
event-loop, coroutine functions, coroutine objects, tasks & ``await``.
32+
event loop, coroutine functions, coroutine objects, tasks and ``await``.
3333

3434

3535
==========
3636
Event Loop
3737
==========
3838

39-
Everything in asyncio happens relative to the event-loop.
39+
Everything in asyncio happens relative to the event loop.
4040
It's the star of the show and there's only one.
4141
It's kind of like an orchestra conductor or military general.
4242
She's behind the scenes managing resources.
4343
Some power is explicitly granted to her, but a lot of her ability to get things
44-
done comes from the respect & cooperation of her subordinates.
44+
done comes from the respect and cooperation of her subordinates.
4545

46-
In more technical terms, the event-loop contains a queue of tasks to be run.
46+
In more technical terms, the event loop contains a queue of tasks to be run.
4747
Some tasks are added directly by you, and some indirectly by asyncio.
48-
The event-loop pops a task from the queue and invokes it (or gives it control),
48+
The event loop pops a task from the queue and invokes it (or gives it control),
4949
similar to calling a function.
5050
That task then runs.
51-
Once it pauses or completes, it returns control to the event-loop.
52-
The event-loop will then move on to the next task in its queue and invoke it.
51+
Once it pauses or completes, it returns control to the event loop.
52+
The event loop will then move on to the next task in its queue and invoke it.
5353
This process repeats indefinitely.
54-
Even if the queue is empty, the event-loop continues to cycle (somewhat aimlessly).
54+
Even if the queue is empty, the event loop continues to cycle (somewhat aimlessly).
5555

5656
Effective overall execution relies on tasks sharing well.
5757
A greedy task could hog control and leave the other tasks to starve rendering
58-
the overall event-loop approach rather useless.
58+
the overall event loop approach rather useless.
5959

6060
::
6161

6262
import asyncio
6363

64-
# This creates an event-loop and indefinitely cycles through
64+
# This creates an event loop and indefinitely cycles through
6565
# its queue of tasks.
6666
event_loop = asyncio.new_event_loop()
6767
event_loop.run_forever()
6868

69-
===================================
70-
Asynchronous Functions & Coroutines
71-
===================================
69+
=====================================
70+
Asynchronous Functions and Coroutines
71+
=====================================
7272

7373
This is a regular 'ol Python function::
7474

7575
def hello_printer():
7676
print(
7777
"Hi, I am a lowly, simple printer, though I have all I "
78-
"need in life -- \nfresh paper & a loving octopus-wife."
78+
"need in life -- \nfresh paper and a loving octopus-wife."
7979
)
8080

8181
Calling a regular function invokes its logic or body::
8282

8383
>>> hello_printer()
8484
Hi, I am a lowly, simple printer, though I have all I need in life --
85-
fresh paper & a loving octopus-wife.
85+
fresh paper and a loving octopus-wife.
8686
>>>
8787

8888
This is an asynchronous-function or coroutine-function::
@@ -93,74 +93,77 @@ This is an asynchronous-function or coroutine-function::
9393
f"By the way, my lucky number is: {magic_number}."
9494
)
9595

96-
Calling an asynchronous function creates and returns a coroutine object. It
97-
does not execute the function::
96+
Calling an asynchronous function creates and returns a
97+
:ref:`coroutine <coroutine>` object.
98+
It does not execute the function::
9899

99100
>>> special_fella(magic_number=3)
100101
<coroutine object special_fella at 0x104ed2740>
101102
>>>
102103

103104
The terms "asynchronous function" (or "coroutine function") and "coroutine object"
104105
are often conflated as coroutine.
105-
I find that a tad confusing.
106+
That can be confusing!
106107
In this article, coroutine will exclusively mean "coroutine object" -- the
107108
thing produced by executing a coroutine function.
108109

109110
That coroutine represents the function's body or logic.
110111
A coroutine has to be explicitly started; again, merely creating the coroutine
111112
does not start it.
112-
Notably, the coroutine can be paused & resumed at various points within the
113+
Notably, the coroutine can be paused and resumed at various points within the
113114
function's body.
114-
That pausing & resuming ability is what allows for asynchronous behavior!
115+
That pausing and resuming ability is what allows for asynchronous behavior!
115116

116117
=====
117118
Tasks
118119
=====
119120

120-
Roughly speaking, tasks are coroutines (not coroutine functions) tied to an
121-
event-loop.
121+
Roughly speaking, :ref:`tasks <asyncio-task-obj>` are coroutines (not coroutine
122+
functions) tied to an event loop.
122123
A task also maintains a list of callback functions whose importance will become
123124
clear in a moment when we discuss ``await``.
124-
When tasks are created they are automatically added to the event-loop's queue
125+
When tasks are created they are automatically added to the event loop's queue
125126
of tasks::
126127

127-
# This creates a Task object and puts it on the event-loop's queue.
128+
# This creates a Task object and puts it on the event loop's queue.
128129
special_task = asyncio.Task(coro=special_fella(magic_number=5), loop=event_loop)
129130

130-
It's common to see a task instantiated without explicitly specifying the event-loop
131+
It's common to see a task instantiated without explicitly specifying the event loop
131132
it belongs to.
132-
Since there's only one event-loop (a global singleton), asyncio made the loop
133-
argument optional and will add it for you if it's left unspecified::
133+
Since there's only one event loop, asyncio made the loop argument optional and
134+
will add it for you if it's left unspecified::
134135

135-
# This creates another Task object and puts it on the event-loop's queue.
136-
# The task is implicitly tied to the event-loop by asyncio since the
136+
# This creates another Task object and puts it on the event loop's queue.
137+
# The task is implicitly tied to the event loop by asyncio since the
137138
# loop argument was left unspecified.
138139
another_special_task = asyncio.Task(coro=special_fella(magic_number=12))
139140

140141
=====
141142
await
142143
=====
143144

144-
``await`` is a Python keyword that's commonly used in one of two different ways::
145+
146+
:keyword:`await` is a Python keyword that's commonly used in one of two
147+
different ways::
145148

146149
await task
147150
await coroutine
148151

149152
Unfortunately, it actually does matter which type of object await is applied to.
150153

151-
``await``-ing a task will cede control from the current task or coroutine to
152-
the event-loop.
154+
``await``\ ing a task will cede control from the current task or coroutine to
155+
the event loop.
153156
And while doing so, add a callback to the awaited task's list of callbacks
154157
indicating it should resume the current task/coroutine when it (the
155-
``await``-ed one) finishes.
158+
``await``\ ed one) finishes.
156159
Said another way, when that awaited task finishes, it adds the original task
157-
back to the event-loops queue.
160+
back to the event loops queue.
158161

159162
In practice, it's slightly more convoluted, but not by much.
160163
In part 2, we'll walk through the details that make this possible.
161164

162165
**Unlike tasks, await-ing a coroutine does not cede control!**
163-
Wrapping a coroutine in a task first, then ``await``-ing that would cede control.
166+
Wrapping a coroutine in a task first, then ``await``\ ing that would cede control.
164167
The behavior of ``await coroutine`` is effectively the same as invoking a regular,
165168
synchronous Python function.
166169
Consider this program::
@@ -171,7 +174,7 @@ Consider this program::
171174
print("I am coro_a(). Hi!")
172175

173176
async def coro_b():
174-
print("I am coro_b(). I sure hope no one hogs the event-loop...")
177+
print("I am coro_b(). I sure hope no one hogs the event loop...")
175178

176179
async def main():
177180
task_b = asyncio.Task(coro_b())
@@ -183,65 +186,67 @@ Consider this program::
183186
asyncio.run(main())
184187

185188
The first statement in the coroutine ``main()`` creates ``task_b`` and places
186-
it on the event-loops queue.
187-
Then, ``coro_a()`` is repeatedly ``await``-ed. Control never cedes to the
188-
event-loop which is why we see the output of all three ``coro_a()``
189+
it on the event loops queue.
190+
Then, ``coro_a()`` is repeatedly ``await``\ ed. Control never cedes to the
191+
event loop which is why we see the output of all three ``coro_a()``
189192
invocations before ``coro_b()``'s output:
190193

191194
.. code-block:: none
192195
193196
I am coro_a(). Hi!
194197
I am coro_a(). Hi!
195198
I am coro_a(). Hi!
196-
I am coro_b(). I sure hope no one hogs the event-loop...
199+
I am coro_b(). I sure hope no one hogs the event loop...
197200
198201
If we change ``await coro_a()`` to ``await asyncio.Task(coro_a())``, the
199202
behavior changes.
200-
The coroutine ``main()`` cedes control to the event-loop with that statement.
201-
The event-loop then works through its queue, calling ``coro_b()`` and then
203+
The coroutine ``main()`` cedes control to the event loop with that statement.
204+
The event loop then works through its queue, calling ``coro_b()`` and then
202205
``coro_a()`` before resuming the coroutine ``main()``.
203206

204207
.. code-block:: none
205208
206-
I am coro_b(). I sure hope no one hogs the event-loop...
209+
I am coro_b(). I sure hope no one hogs the event loop...
207210
I am coro_a(). Hi!
208211
I am coro_a(). Hi!
209212
I am coro_a(). Hi!
210213
211214
212-
----------------------------------------------
213-
A conceptual overview part 2: the nuts & bolts
214-
----------------------------------------------
215+
------------------------------------------------
216+
A conceptual overview part 2: the nuts and bolts
217+
------------------------------------------------
215218

216219
Part 2 goes into detail on the mechanisms asyncio uses to manage control flow.
217220
This is where the magic happens.
218221
You'll come away from this section knowing what await does behind the scenes
219222
and how to make your own asynchronous operators.
220223

221-
==============================================
222-
coroutine.send(), await, yield & StopIteration
223-
==============================================
224+
================================================
225+
coroutine.send(), await, yield and StopIteration
226+
================================================
224227

225228
asyncio leverages those 4 components to pass around control.
226229

227-
``coroutine.send(arg)`` is the method used to start or resume a coroutine.
230+
231+
232+
:meth:`coroutine.send(arg) <generator.send>` is the method used to start or resume a coroutine.
228233
If the coroutine was paused and is now being resumed, the argument ``arg``
229234
will be sent in as the return value of the ``yield`` statement which originally
230235
paused it.
231236
If the coroutine is being started, as opposed to resumed, ``arg`` must be None.
232237

233-
``yield``, like usual, pauses execution and returns control to the caller.
238+
:ref:`yield <yieldexpr>`, like usual, pauses execution and returns control to the caller.
234239
In the example below, the ``yield`` is on line 3 and the caller is
235240
``... = await rock`` on line 11.
236241
Generally, ``await`` calls the ``__await__`` method of the given object.
237-
``await`` also does one more very special thing: it percolates (or passes along)
242+
``await`` also does one more very special thing: it propagates (or passes along)
238243
any yields it receives up the call-chain.
239244
In this case, that's back to ``... = coroutine.send(None)`` on line 16.
240245

241246
The coroutine is resumed via the ``coroutine.send(42)`` call on line 21.
242-
The coroutine picks back up from where it ``yield``-ed (i.e. paused) on line 3
247+
The coroutine picks back up from where it ``yield``\ ed (that is, paused) on line 3
243248
and executes the remaining statements in its body.
244-
When a coroutine finishes it raises a ``StopIteration`` exception with the
249+
When a coroutine finishes it raises a :exc:`StopIteration` exception with the
245250
return value attached to the exception.
246251

247252
::
@@ -294,7 +299,7 @@ That might sound odd to you. Frankly, it was to me too. You might be thinking:
294299
a generator-coroutine, a different beast entirely.
295300

296301
2. What about a ``yield from`` within the coroutine to a function that yields
297-
(i.e. plain generator)?
302+
(that is, plain generator)?
298303
``SyntaxError: yield from not allowed in a coroutine.``
299304
I imagine Python made this a ``SyntaxError`` to mandate only one way of using
300305
coroutines for the sake of simplicity.
@@ -304,8 +309,8 @@ That might sound odd to you. Frankly, it was to me too. You might be thinking:
304309
Futures
305310
=======
306311

307-
A future is an object meant to represent a computation or process's status and
308-
result.
312+
A :ref:`future <asyncio-future-obj>` is an object meant to represent a
313+
computation or process's status and result.
309314
The term is a nod to the idea of something still to come or not yet happened,
310315
and the object is a way to keep an eye on that something.
311316

@@ -321,15 +326,15 @@ I said in the prior section tasks store a list of callbacks and I lied a bit.
321326
It's actually the ``Future`` class that implements this logic which ``Task``
322327
inherits.
323328

324-
Futures may be also used directly i.e. not via tasks.
329+
Futures may be also used directly that is, not via tasks.
325330
Tasks mark themselves as done when their coroutine's complete.
326331
Futures are much more versatile and will be marked as done when you say so.
327332
In this way, they're the flexible interface for you to make your own conditions
328333
for waiting and resuming.
329334

330-
=========================
331-
await-ing Tasks & futures
332-
=========================
335+
===========================
336+
await-ing Tasks and futures
337+
===========================
333338

334339
``Future`` defines an important method: ``__await__``. Below is the actual
335340
implementation (well, one line was removed for simplicity's sake) found
@@ -352,18 +357,18 @@ in the control-flow example.
352357
11 return self.result()
353358

354359
The ``Task`` class does not override ``Future``'s ``__await__`` implementation.
355-
``await``-ing a task or future invokes that above ``__await__`` method and
360+
``await``\ ing a task or future invokes that above ``__await__`` method and
356361
percolates the ``yield`` on line 6 to relinquish control to its caller, which
357-
is generally the event-loop.
362+
is generally the event loop.
358363

359364
========================
360365
A homemade asyncio.sleep
361366
========================
362367

363368
We'll go through an example of how you could leverage a future to create your
364-
own variant of asynchronous sleep (i.e. asyncio.sleep).
369+
own variant of asynchronous sleep (that is, asyncio.sleep).
365370

366-
This snippet puts a few tasks on the event-loops queue and then ``await``\ s a
371+
This snippet puts a few tasks on the event loops queue and then ``await``\ s a
367372
yet unknown coroutine wrapped in a task: ``async_sleep(3)``.
368373
We want that task to finish only after 3 seconds have elapsed, but without
369374
hogging control while waiting.
@@ -374,7 +379,7 @@ hogging control while waiting.
374379
print(f"I am worker. Work work.")
375380

376381
async def main():
377-
# Add a few other tasks to the event-loop, so there's something
382+
# Add a few other tasks to the event loop, so there's something
378383
# to do while asynchronously sleeping.
379384
work_tasks = [
380385
asyncio.Task(other_work()),
@@ -407,28 +412,28 @@ will monitor how much time has elapsed and accordingly call
407412
async def async_sleep(seconds: float):
408413
future = asyncio.Future()
409414
time_to_wake = time.time() + seconds
410-
# Add the watcher-task to the event-loop.
415+
# Add the watcher-task to the event loop.
411416
watcher_task = asyncio.Task(_sleep_watcher(future, time_to_wake))
412417
# Block until the future is marked as done.
413418
await future
414419

415420

416421
We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its
417-
``__await__`` in order to cede control to the event-loop.
422+
``__await__`` in order to cede control to the event loop.
418423
This is effectively the same as calling ``asyncio.sleep(0)``, but I prefer the
419424
clarity this approach offers, not to mention it's somewhat cheating to use
420425
``asyncio.sleep`` when showcasing how to implement it!
421426

422-
The event-loop, as usual, cycles through its queue of tasks, giving them control,
427+
The event loop, as usual, cycles through its queue of tasks, giving them control,
423428
and receiving control back when each task pauses or finishes.
424429
The ``watcher_task``, which runs the coroutine: ``_sleep_watcher(...)`` will be
425-
invoked once per full cycle of the event-loop's queue.
430+
invoked once per full cycle of the event loop's queue.
426431
On each resumption, it'll check the time and if not enough has elapsed, it'll
427-
pause once again and return control to the event-loop.
432+
pause once again and return control to the event loop.
428433
Eventually, enough time will have elapsed, and ``_sleep_watcher(...)`` will
429434
mark the future as done, and then itself finish too by breaking out of the
430435
infinite while loop.
431-
Given this helper task is only invoked once per cycle of the event-loop's queue,
436+
Given this helper task is only invoked once per cycle of the event loop's queue,
432437
you'd be correct to note that this asynchronous sleep will sleep **at least**
433438
three seconds, rather than exactly three seconds.
434439
Note, this is also of true of the library-provided asynchronous function:

0 commit comments

Comments
 (0)