11.. _a-conceputal-overview-of-asyncio :
22
3- *********************************
3+ ********************************
44A Conceptual Overview of asyncio
5- *********************************
5+ ********************************
66
77:Author: Alexander Nordin
88
@@ -16,7 +16,7 @@ curiosity (read: drove me nuts).
1616You should be able to comfortably answer all these questions by the end
1717of 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
3131In 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==========
3636Event Loop
3737==========
3838
39- Everything in asyncio happens relative to the event- loop.
39+ Everything in asyncio happens relative to the event loop.
4040It's the star of the show and there's only one.
4141It's kind of like an orchestra conductor or military general.
4242She's behind the scenes managing resources.
4343Some 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.
4747Some 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),
4949similar to calling a function.
5050That 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.
5353This 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
5656Effective overall execution relies on tasks sharing well.
5757A 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
7373This 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
8181Calling 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
8888This 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
103104The terms "asynchronous function" (or "coroutine function") and "coroutine object"
104105are often conflated as coroutine.
105- I find that a tad confusing.
106+ That can be confusing!
106107In this article, coroutine will exclusively mean "coroutine object" -- the
107108thing produced by executing a coroutine function.
108109
109110That coroutine represents the function's body or logic.
110111A coroutine has to be explicitly started; again, merely creating the coroutine
111112does 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
113114function'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=====
117118Tasks
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.
122123A task also maintains a list of callback functions whose importance will become
123124clear 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
125126of 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
131132it 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=====
141142await
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
149152Unfortunately, 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.
153156And while doing so, add a callback to the awaited task's list of callbacks
154157indicating it should resume the current task/coroutine when it (the
155- ``await ``- ed one) finishes.
158+ ``await ``\ ed one) finishes.
156159Said 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
159162In practice, it's slightly more convoluted, but not by much.
160163In 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.
164167The behavior of ``await coroutine `` is effectively the same as invoking a regular,
165168synchronous Python function.
166169Consider 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
185188The 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() ``
189192invocations 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
199202behavior 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
216219Part 2 goes into detail on the mechanisms asyncio uses to manage control flow.
217220This is where the magic happens.
218221You'll come away from this section knowing what await does behind the scenes
219222and 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
225228asyncio 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.
228233If the coroutine was paused and is now being resumed, the argument ``arg ``
229234will be sent in as the return value of the ``yield `` statement which originally
230235paused it.
231236If 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.
234239In the example below, the ``yield `` is on line 3 and the caller is
235240``... = await rock `` on line 11.
236241Generally, ``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)
238243any yields it receives up the call-chain.
239244In this case, that's back to ``... = coroutine.send(None) `` on line 16.
240245
241246The 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
243248and 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
245250return 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:
304309Futures
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.
309314The term is a nod to the idea of something still to come or not yet happened,
310315and 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.
321326It's actually the ``Future `` class that implements this logic which ``Task ``
322327inherits.
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.
325330Tasks mark themselves as done when their coroutine's complete.
326331Futures are much more versatile and will be marked as done when you say so.
327332In this way, they're the flexible interface for you to make your own conditions
328333for 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
335340implementation (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
354359The ``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
356361percolates 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========================
360365A homemade asyncio.sleep
361366========================
362367
363368We'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
367372yet unknown coroutine wrapped in a task: ``async_sleep(3) ``.
368373We want that task to finish only after 3 seconds have elapsed, but without
369374hogging 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
416421We'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.
418423This is effectively the same as calling ``asyncio.sleep(0) ``, but I prefer the
419424clarity 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,
423428and receiving control back when each task pauses or finishes.
424429The ``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.
426431On 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.
428433Eventually, enough time will have elapsed, and ``_sleep_watcher(...) `` will
429434mark the future as done, and then itself finish too by breaking out of the
430435infinite 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,
432437you'd be correct to note that this asynchronous sleep will sleep **at least **
433438three seconds, rather than exactly three seconds.
434439Note, this is also of true of the library-provided asynchronous function:
0 commit comments