From 05b35b08d7f254fac394e540c65552abffa9eda6 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Tue, 29 Jul 2025 11:11:21 -0700 Subject: [PATCH 01/70] - Add an explainer guide (aka HOWTO, not how-to) for asyncio. --- .../a-conceptual-overview-of-asyncio.rst | 736 ++++++++++++++++++ Doc/howto/index.rst | 2 + Doc/library/asyncio.rst | 3 + 3 files changed, 741 insertions(+) create mode 100644 Doc/howto/a-conceptual-overview-of-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 index 00000000000000..8c3fd802eb971f --- /dev/null +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -0,0 +1,736 @@ +.. _a-conceputal-overview-of-asyncio: + +********************************* +A Conceptual Overview of asyncio +********************************* + +:Author: Alexander Nordin + +This article seeks to help you build a sturdy mental model of how asyncio +fundamentally works. Something that will help you understand the how and why +behind the recommended patterns. The final section, :ref:`which_concurrency_do_I_want`, zooms out a bit and compares the +common approaches to concurrency -- multiprocessing, multithreading & asyncio -- and describes where +each is most useful. + +During my own asyncio learning process, a few aspects particually drove my curiosity (read: drove me nuts). You +should be able to comfortably answer all these questions by the end of this article. + +- What's roughly happening behind the scenes when an object is ``await``-ed? +- How does asyncio differentiate between a task which doesn't need CPU-time to make progress towards completion, for example, a network request or file read as opposed to a task that does need cpu-time to make progress, like computing n-factorial? +- How would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, etc. + +The first two sections feature some examples but are generally focused on theory and explaining concepts. +The next two sections are centered around examples, focused on further illustrating and reinforcing ideas +practically. + +.. contents:: Sections + :depth: 1 + :local: + +--------------------------------------------- +A conceptual overview part 1: the high-level +--------------------------------------------- + +In part 1, we'll cover the main, high-level building blocks of asyncio: the event-loop, +coroutine functions, coroutine objects, tasks & ``await``. + + +========================== +Event Loop +========================== + +Everything in asyncio happens relative to the event-loop. It's the star of the show and there's only one. +It's kind of like an orchestra conductor or military general. She's behind the scenes managing resources. +Some power is explicitly granted to her, but a lot of her ability to get things done comes from the respect +& cooperation of her subordinates. + +In more technical terms, the event-loop contains a queue of tasks to be run. Some tasks are added directly +by you, and some indirectly by asyncio. The event-loop pops a task from the queue and invokes it (or gives +it control), similar to calling a function. That task then runs. Once it pauses or completes, it returns +control to the event-loop. The event-loop will then move on to the next task in its queue and invoke it. +This process repeats indefinitely. Even if the queue is empty, the event-loop continues to cycle +(somewhat aimlessly). + +Effective overall execution relies on tasks sharing well. A greedy task could hog control and leave the +other tasks to starve rendering the overall event-loop approach rather useless. + +:: + + import asyncio + + # This creates an event-loop and indefinitely cycles through + # its queue of tasks. + event_loop = asyncio.new_event_loop() + event_loop.run_forever() + +==================================== +Asynchronous Functions & Coroutines +==================================== + +This is a regular 'ol Python function:: + + def hello_printer(): + print( + "Hi, I am a lowly, simple printer, though I have all I " + "need in life -- \nfresh paper & a loving octopus-wife." + ) + +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 & a loving octopus-wife. + >>> + +This is an asynchronous-function or coroutine-function:: + + async def special_fella(magic_number: int): + print( + "I am a super special function. Far cooler than that printer. " + f"By the way, my lucky number is: {magic_number}." + ) + +Calling an asynchronous function creates and returns a coroutine object. It does not execute the function:: + + >>> special_fella(magic_number=3) + + >>> + +The terms "asynchronous function" (or "coroutine function") and "coroutine object" are often conflated +as coroutine. I find that a tad confusing. In this article, coroutine will exclusively mean "coroutine object" +-- the thing produced by executing a coroutine function. + +That 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 & +resumed at various points within the function's body. That pausing & resuming ability is what allows +for asynchronous behavior! + +=========== +Tasks +=========== + +Roughly speaking, tasks 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 ``await``. When tasks are created +they are automatically added to the event-loop's queue of tasks:: + + # This creates a Task object and puts it on the event-loop's queue. + special_task = asyncio.Task(coro=special_fella(magic_number=5), loop=event_loop) + +It's common to see a task instantiated without explicitly specifying the event-loop it belongs to. Since +there's only one event-loop (a global singleton), asyncio made the loop argument optional and will add it +for you if it's left unspecified:: + + # This creates another Task object and puts it on the event-loop's queue. + # The task is implicitly tied to the event-loop by asyncio since the + # loop argument was left unspecified. + another_special_task = asyncio.Task(coro=special_fella(magic_number=12)) + +=========== +await +=========== + +``await`` is a Python keyword that's commonly used in one of two different ways:: + + await task + await coroutine + +Unfortunately, it actually does matter which type of object await is applied to. + +``await``-ing a task will cede control from the current task or coroutine to the event-loop. And while doing so, +add a callback to the awaited task's list of callbacks indicating it should resume the current task/coroutine +when it (the ``await``-ed one) finishes. Said another way, when that awaited task finishes, it adds the original task +back to the event-loops queue. + +In practice, it's slightly more convoluted, but not by much. In part 2, we'll walk through the +details that make this possible. And in the control flow analysis example we'll walk through, in precise detail, +the various control handoffs in an example async program. + +**Unlike tasks, await-ing a coroutine does not cede control!** Wrapping a coroutine in a task first, then ``await``-ing +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.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 places it on the event-loops queue. Then, +``coro_a()`` is repeatedly ``await``-ed. 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.Task(coro_a())``, the behavior changes. The coroutine +``main()`` cedes control to the event-loop with that statement. The event-loop then works through its queue, +calling ``coro_b()`` and then ``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! + + +---------------------------------------------- +A conceptual overview part 2: the nuts & bolts +---------------------------------------------- + +Part 2 goes into detail on the mechanisms 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. + +=============================================== +coroutine.send(), await, yield & StopIteration +=============================================== + +asyncio leverages those 4 components to pass around control. + +``coroutine.send(arg)`` 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 started, as opposed to resumed, ``arg`` must be None. + +``yield``, like usual, pauses execution and returns control to the caller. In the example below, the ``yield`` is on line 3 +and the caller is ``... = await rock`` on line 11. Generally, ``await`` calls the ``__await__`` method of the given object. +``await`` also does one more very special thing: it percolates (or passes along) any yields 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 (i.e. paused) on line 3 and executes the remaining statements in its body. When a coroutine finishes +it raises a ``StopIteration`` exception with the return value attached to the exception. + +:: + + 1 class Rock: + 2 def __await__(self): + 3 value_sent_in = yield 7 + 4 print(f"Rock.__await__ resuming with value: {value_sent_in}.") + 5 return value_sent_in + 6 + 7 async def main(): + 8 print("Beginning coroutine main().") + 9 rock = Rock() + 10 print("Awaiting rock...") + 11 value_from_rock = await rock + 12 print(f"Coroutine received value: {value_from_rock} from rock.") + 13 return 23 + 14 + 15 coroutine = main() + 16 intermediate_result = coroutine.send(None) + 17 print(f"Coroutine paused and returned intermediate value: {intermediate_result}.") + 18 + 19 print(f"Resuming coroutine and sending in value: 42.") + 20 try: + 21 coroutine.send(42) + 22 except StopIteration as e: + 23 returned_value = e.value + 24 print(f"Coroutine main() finished and provided value: {returned_value}.") + +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 control flow and values were passed. + +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. Frankly, it was to me too. You might be thinking: + + 1. What about a ``yield`` directly within the coroutine? The coroutine becomes + a generator-coroutine, a different beast entirely. + + 2. What about a ``yield from`` within the coroutine to a function that yields + (i.e. plain generator)? ``SyntaxError: yield from not allowed in a coroutine.`` + I imagine Python made this a ``SyntaxError`` to mandate only one way of using coroutines + for the sake of simplicity. Ideologically, ``yield from`` and ``await`` are quite similar. + +=========== +Futures +=========== + +A future is an object meant to represent a computation or process'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. To be clear, a +future does not represent the actual computation to be done, like a coroutine does, instead it +represents the status and result of that computation, kind of like a status-light +(red, yellow or green) or indicator. + +``Task`` subclasses ``Future`` in order to gain these various capabilities. I said in the prior section tasks store a list +of callbacks and I lied a bit. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. + +Futures may be also used directly i.e. not via tasks. Tasks mark themselves as done when their coroutine's +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. + +========================== +await-ing Tasks & futures +========================== + +``Future`` defines an important method: ``__await__``. Below is the actual implementation (well, one line was +removed for simplicity's sake) found in ``asyncio.futures.Future``. It's okay if it doesn't make complete sense +now, we'll go through it in detail in the control-flow example. + +:: + + 1 class Future: + 2 ... + 3 def __await__(self): + 4 + 5 if not self.done(): + 6 yield self + 7 + 8 if not self.done(): + 9 raise RuntimeError("await wasn't used with future") + 10 + 11 return self.result() + +The ``Task`` class does not override ``Future``'s ``__await__`` implementation. ``await``-ing a task or future +invokes that above ``__await__`` method and percolates the ``yield`` on line 6 to relinquish +control to its caller, which is generally the event-loop. + +======================== +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 (i.e. asyncio.sleep). + +This snippet puts a few tasks on the event-loops queue and then ``await``\ s a +yet unknown coroutine wrapped in a task: ``async_sleep(3)``. We want that task to finish only after +3 seconds have elapsed, but without hogging control while waiting. + +:: + + async def other_work(): + print(f"I am worker. 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.Task(other_work()), + asyncio.Task(other_work()), + asyncio.Task(other_work()) + ] + print( + "Beginning asynchronous sleep at time: " + f"{datetime.datetime.now().strftime("%H:%M:%S")}." + ) + await asyncio.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 ``future.set_result()``, +the method responsible for marking that future as done, is never called, 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.Task(_sleep_watcher(future, time_to_wake)) + # Block until the future is marked as done. + await future + + +We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its ``__await__`` in order +to cede control to the event-loop. This is effectively the same as calling ``asyncio.sleep(0)``, +but I prefer the clarity this approach offers, not to mention it's somewhat cheating to use +``asyncio.sleep`` when showcasing how to implement it! + +The event-loop, as usual, cycles through its queue of tasks, giving them control, and receiving +control back when each task pauses or finishes. The ``watcher_task``, which runs the coroutine: ``_sleep_watcher(...)`` +will be invoked once per full cycle of the event-loop's queue. On each resumption, it'll check the time and +if not enough has elapsed, it'll pause once again and return control 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's +queue, 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 the library-provided asynchronous +function: ``asyncio.sleep``. + +:: + + class YieldToEventLoop: + def __await__(self): + yield + + async def _sleep_watcher(future: asyncio.Future, time_to_wake: float): + 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 am worker. Work work. + I am worker. Work work. + I am worker. 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. I wanted 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() + + +.. _anaylzing-control-flow-example: + +---------------------------------------------- +Analyzing an example programs control flow +---------------------------------------------- + +We'll walkthrough, step by step, a simple asynchronous program following along in the key methods of Task & Future that are leveraged when asyncio is orchestrating the show. + + +=============== +Task.step +=============== + +The actual method that invokes a Tasks coroutine: ``asyncio.tasks.Task.__step_run_and_handle_result`` +is about 80 lines long. For the sake of clarity, I've removed all of the edge-case error handling, +simplified some aspects and renamed it, but the core logic remains unchanged. + +:: + + 1 class Task(Future): + 2 ... + 3 def step(self): + 4 try: + 5 awaited_task = self.coro.send(None) + 6 except StopIteration as e: + 7 super().set_result(e.value) + 8 else: + 9 awaited_task.add_done_callback(self.__step) + 10 ... + + +====================== +Example program +====================== + +:: + + # Filename: program.py + 1 async def triple(val: int): + 2 return val * 3 + 3 + 4 async def main(): + 5 triple_task = asyncio.Task(coro=triple(val=5)) + 6 tripled_val = await triple_task + 7 return tripled_val + 2 + 8 + 9 loop = asyncio.new_event_loop() + 10 main_task = asyncio.Task(main(), loop=loop) + 11 loop.run_forever() + +===================== +Control flow +===================== + +At a high-level, this is how control flows: + +.. code-block:: none + + 1 program + 2 event-loop + 3 main_task.step + 4 main() + 5 triple_task.__await__ + 6 main() + 7 main_task.step + 8 event-loop + 9 triple_task.step + 10 triple() + 11 triple_task.step + 12 event-loop + 13 main_task.step + 14 triple_task.__await__ + 15 main() + 16 main_task.step + 17 event-loop + +And, in much more detail: + +1. Control begins in ``program.py`` + Line 9 creates an event-loop, line 10 creates ``main_task`` and adds it to + the event-loop, line 11 indefinitely passes control to the event-loop. +2. Control is now in the event-loop + The event-loop pops ``main_task`` off its queue, then invokes it by calling ``main_task.step()``. +3. Control is now in ``main_task.step`` + We enter the try-block on line 4 then begin the coroutine ``main()`` on line 5. +4. Control is now in the coroutine: ``main()`` + The Task ``triple_task`` is created on line 5 which adds it to the event-loops queue. Line 6 + ``await``\ s triple_task. Remember, that calls ``Task.__await__`` then percolates any ``yield``\ s. +5. Control is now in ``triple_task.__await__`` + ``triple_task`` is not done given it was just created, so we enter the first if-block on line 5 and ``yield`` the thing + we'll be waiting for -- ``triple_task``. +6. Control is now in the coroutine: ``main()`` + ``await`` percolates the ``yield`` and the yielded value -- ``triple_task``. +7. Control is now in ``main_task.step`` + The variable ``awaited_task`` is ``triple_task``. No ``StopIteration`` was raised so the else in the try-block + on line 8 executes. A done-callback: ``main_task.step`` is added to the ``triple_task``. The step method ends and + returns to the event-loop. +8. Control is now in the event-loop + The event-loop cycles to the next task in its queue. The event-loop pops ``triple_task`` from its queue and invokes + it by calling ``triple_task.step()``. +9. Control is now in ``triple_task.step`` + We enter the try-block on line 4 then begin the coroutine ``triple()`` via line 5. +10. Control is now in the coroutine: ``triple()`` + It computes 3 times 5, then finishes and raises a ``StopIteration`` exception. +11. Control is now in ``triple_task.step`` + The ``StopIteration`` exception is caught so we go to line 7. The return value of the coroutine ``triple()`` is embedded in the + value attribute of that exception. ``Future.set_result()`` saves the result, marks the task as done and adds + the done-callbacks of ``triple_task`` to the event-loops queue. The step method ends and returns control to the + event-loop. +12. Control is now in the event-loop + The event-loop cycles to the next task in its queue. The event-loop pops ``main_task`` and resumes it by + calling ``main_task.step()``. +13. Control is now in ``main_task.step`` + We enter the try-block on line 4 then resume the coroutine ``main`` which will pick up again from where it + ``yield``-ed. Recall, it ``yield``-ed not in the coroutine, but in ``triple_task.__await__`` on line 6. +14. Control is now in ``triple_task.__await__`` + We evaluate the if-statement on line 8 which ensures that ``triple_task`` was completed. Then, it returns the + result of ``triple_task`` which was saved earlier. Finally that result is returned to the + caller (i.e. ``... = await triple_task``). +15. Control is now in the coroutine: ``main()`` + ``tripled_val`` is 15. The coroutine finishes and raises a ``StopIteration`` exception with the return value of 17 attached. +16. Control is now in ``main_task.step`` + The ``StopIteration`` exception is caught and ``main_task`` is marked as done and its result is saved. The step method ends + and returns control to the event-loop. +17. Control is now in the event-loop + There's nothing in the queue. The event-loop cycles aimlessly onwards. + +---------------------------------------------- +Barebones network I/O example +---------------------------------------------- + +Here we'll see a simple but thorough example showing how asyncio can offer an advantage over serial programs. The example doesn't rely on any asyncio +operators (besides the event-loop). It's all non-blocking sockets & custom awaitables that help you see what's actually happening under the +hood and how you could do something similar. + +Performing a database request across a network might take half a second or so, but that's ages in computer-time. Your processor could have +done millions or even billions of things. The same is true for, say, requesting a website, downloading a car, loading a file from disk +into memory, etc. The general theme is those are all input/output (I/O) actions. + +Consider performing two tasks: requesting some information from a server and doing some computation locally. A serial approach would +look like: ping the server, idle while waiting for a response, receive the response, perform the local computation. An asynchronous +approach would look like: ping the server, do some of the local computation while waiting for a response, check if the server is +ready yet, do a bit more of the local computation, check again, etc. Basically we're freeing up the CPU to do other activities +instead of scratching its belly button. + +This example has a server (a separate, local process) compute the sum of many samples from a Gaussian (i.e. normal) distribution. +And the local computation finds the sum of many samples from a uniform distribution. As you'll see, the asynchronous approach +runs notably faster, since progress can be made on computing the sum of many uniform samples, while waiting for the server to +calculate and respond. + +===================== +Serial output +===================== + +.. code-block:: none + + $ python serial_approach.py + Beginning server_request. + ====== Done server_request. total: -2869.04. Ran for: 2.77s. ====== + Beginning uniform_sum. + ====== Done uniform_sum. total: 60001676.02. Ran for: 4.77s. ====== + Total time elapsed: 7.54s. + +===================== +Asynchronous output +===================== + +.. code-block:: none + + $ python async_approach.py + Beginning uniform_sum. + Pausing uniform_sum at sample_num: 26,999,999. time_elapsed: 1.01s. + + Beginning server_request. + Pausing server_request. time_elapsed: 0.00s. + + Resuming uniform_sum. + Pausing uniform_sum at sample_num: 53,999,999. time_elapsed: 1.05s. + + Resuming server_request. + Pausing server_request. time_elapsed: 0.00s. + + Resuming uniform_sum. + Pausing uniform_sum at sample_num: 80,999,999. time_elapsed: 1.05s. + + Resuming server_request. + Pausing server_request. time_elapsed: 0.00s. + + Resuming uniform_sum. + Pausing uniform_sum at sample_num: 107,999,999. time_elapsed: 1.04s. + + Resuming server_request. + ====== Done server_request. total: -2722.46. ====== + + Resuming uniform_sum. + ====== Done uniform_sum. total: 59999087.62 ====== + + Total time elapsed: 4.60s. + +====================== +Code +====================== + +Now, we'll explore some of the most important snippets. + +Below is the portion of the asynchronous approach responsible for checking if the server's done yet. And, +if not, yielding control back to the event-loop instead of idly waiting. I'd like to draw your attention +to a specific part of this snippet. Setting a socket to non-blocking mode means the ``recv()`` call +won't idle while waiting for a response. Instead, if there's no data to be read, it'll immediately +raise a ``BlockingIOError``. If there is data available, the ``recv()`` will proceed as normal. + +.. code-block:: python + + class YieldToEventLoop: + def __await__(self): + yield + ... + + async def get_server_data(): + client = socket.socket() + client.connect(server.SERVER_ADDRESS) + client.setblocking(False) + + while True: + try: + # For reference, the first argument to recv() is the maximum number + # of bytes to attempt to read. Setting it to 4096 means we could get 2 + # bytes or 4 bytes, or even 4091 bytes, but not 4097 bytes back. + # However, if there are no bytes available to be read, this recv() + # will raise a BlockingIOError since the socket was set to + # non-blocking mode. + response = client.recv(4096) + break + except BlockingIOError: + await YieldToEventLoop() + return response + + +And this is the portion of code responsible for asynchronously computing +the uniform sums. It's designed to allow for working through the sum a portion at a time. +The ``time_allotment`` argument to the coroutine function decides how long the sum +function will iterate, in other words, synchronously hog control, before ceding +back to the event-loop. + +.. code-block:: python + + async def uniform_sum(n_samples: int, time_allotment: float) -> float: + + start_time = time.time() + + total = 0.0 + for _ in range(n_samples): + total += random.random() + + time_elapsed = time.time() - start_time + if time_elapsed > time_allotment: + await YieldToEventLoop() + start_time = time.time() + + return total + +The above snippet was simplified a bit. Reading ``time.time()`` and evaluating an if-condition +on every iteration for many, many iterations (in this case roughly a hundred million) more than +eats up the runtime savings associated with the asynchronous approach. The actual +implementation involves chunking the iteration, so you only perform the check +every few million iterations. With that change, the asynchronous approach wins in a landslide. +This is important to keep in mind. Too much checking or constantly jumping between tasks can +ultimately cause more harm than good! + +The server, async & serial programs are available in full here: +https://github.com/anordin95/a-conceptual-overview-of-asyncio/tree/main/barebones-network-io-example. + +.. _which_concurrency_do_I_want: + +------------------------------ +Which concurrency do I want +------------------------------ + +=========================== +multiprocessing +=========================== + +For any computationally bound work in Python, you likely want to use multiprocessing. +Otherwise, the Global Interpreter Lock (GIL) will generally get in your way! For those who don't +know, the GIL is a lock which ensures only one Python instruction is executed at a time. +Of course, since processes are generally entirely independent from one another, the GIL in one +process won't be impeded by the GIL in another process. Granted, I believe there are ways to +also get around the GIL in a single process by leveraging C extensions. + +=========================== +multithreading & asyncio +=========================== + +Multithreading and asyncio are much more similar in where they're useful with Python: not at all for +computationally-bound work, and crucially for I/O bound work. For applications that need to +manage absolutely tons of distinct I/O connections or chunks-of-work, asyncio is a must. For example, +a web server handling thousands of requests "simultaneously" (in quotes, because, as we saw, the frequent +handoffs of control only create the illusion of simultaneous execution). Otherwise, I think the choice +between which to use is somewhat down to taste. + +Multithreading maintains an OS managed thread for each chunk of work. Whereas asyncio uses Tasks for +each work-chunk and manages them via the event-loop's queue. I believe the marginal overhead of one more +chunk of work is a fair bit lower for asyncio than threads, which matters a lot for applications that +need to manage many, many chunks of work. + +There are some other benefits associated with using asyncio. One is clearer visibility into when and where +interleaving occurs. The code chunk between two awaits is certainly synchronous. Another is simpler debugging, +since it's easier to attach and follow a trace and reason about code execution. With threading, the interleaving +is more of a black-box. One benefit of multithreading is not really having to worry about greedy threads +hogging execution, something that could happen with asyncio where a greedy coroutine never awaits and +effectively stalls the event-loop. diff --git a/Doc/howto/index.rst b/Doc/howto/index.rst index f350141004c2db..9a7bb0573c98a7 100644 --- a/Doc/howto/index.rst +++ b/Doc/howto/index.rst @@ -11,6 +11,7 @@ Python Library Reference. :maxdepth: 1 :hidden: + a-conceptual-overview-of-asyncio.rst cporting.rst curses.rst descriptor.rst @@ -38,6 +39,7 @@ Python Library Reference. General: +* :ref:`a-conceputal-overview-of-asyncio` * :ref:`annotations-howto` * :ref:`argparse-tutorial` * :ref:`descriptorhowto` diff --git a/Doc/library/asyncio.rst b/Doc/library/asyncio.rst index 7d368dae49dc1d..a38f4199b53676 100644 --- a/Doc/library/asyncio.rst +++ b/Doc/library/asyncio.rst @@ -29,6 +29,9 @@ database connection libraries, distributed task queues, etc. asyncio is often a perfect fit for IO-bound and high-level **structured** network code. +If you're new to asyncio or confused by it and would like to better understand the fundmentals of how +it works check out: :ref:`a-conceputal-overview-of-asyncio`. + asyncio provides a set of **high-level** APIs to: * :ref:`run Python coroutines ` concurrently and From 446534cf2f6e6541e9a55a2a166d02026372c894 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Tue, 29 Jul 2025 11:32:16 -0700 Subject: [PATCH 02/70] Fix linter errors. --- .../a-conceptual-overview-of-asyncio.rst | 294 +++++++++--------- 1 file changed, 147 insertions(+), 147 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 8c3fd802eb971f..ca40e2e59d1b9f 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -7,20 +7,20 @@ A Conceptual Overview of asyncio :Author: Alexander Nordin This article seeks to help you build a sturdy mental model of how asyncio -fundamentally works. Something that will help you understand the how and why -behind the recommended patterns. The final section, :ref:`which_concurrency_do_I_want`, zooms out a bit and compares the -common approaches to concurrency -- multiprocessing, multithreading & asyncio -- and describes where +fundamentally works. Something that will help you understand the how and why +behind the recommended patterns. The final section, :ref:`which_concurrency_do_I_want`, zooms out a bit and compares the +common approaches to concurrency -- multiprocessing, multithreading & asyncio -- and describes where each is most useful. -During my own asyncio learning process, a few aspects particually drove my curiosity (read: drove me nuts). You +During my own asyncio learning process, a few aspects particually drove my curiosity (read: drove me nuts). You should be able to comfortably answer all these questions by the end of this article. - What's roughly happening behind the scenes when an object is ``await``-ed? - How does asyncio differentiate between a task which doesn't need CPU-time to make progress towards completion, for example, a network request or file read as opposed to a task that does need cpu-time to make progress, like computing n-factorial? - How would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, etc. -The first two sections feature some examples but are generally focused on theory and explaining concepts. -The next two sections are centered around examples, focused on further illustrating and reinforcing ideas +The first two sections feature some examples but are generally focused on theory and explaining concepts. +The next two sections are centered around examples, focused on further illustrating and reinforcing ideas practically. .. contents:: Sections @@ -31,7 +31,7 @@ practically. A conceptual overview part 1: the high-level --------------------------------------------- -In part 1, we'll cover the main, high-level building blocks of asyncio: the event-loop, +In part 1, we'll cover the main, high-level building blocks of asyncio: the event-loop, coroutine functions, coroutine objects, tasks & ``await``. @@ -39,26 +39,26 @@ coroutine functions, coroutine objects, tasks & ``await``. Event Loop ========================== -Everything in asyncio happens relative to the event-loop. It's the star of the show and there's only one. -It's kind of like an orchestra conductor or military general. She's behind the scenes managing resources. -Some power is explicitly granted to her, but a lot of her ability to get things done comes from the respect +Everything in asyncio happens relative to the event-loop. It's the star of the show and there's only one. +It's kind of like an orchestra conductor or military general. She's behind the scenes managing resources. +Some power is explicitly granted to her, but a lot of her ability to get things done comes from the respect & cooperation of her subordinates. -In more technical terms, the event-loop contains a queue of tasks to be run. Some tasks are added directly -by you, and some indirectly by asyncio. The event-loop pops a task from the queue and invokes it (or gives -it control), similar to calling a function. That task then runs. Once it pauses or completes, it returns -control to the event-loop. The event-loop will then move on to the next task in its queue and invoke it. -This process repeats indefinitely. Even if the queue is empty, the event-loop continues to cycle +In more technical terms, the event-loop contains a queue of tasks to be run. Some tasks are added directly +by you, and some indirectly by asyncio. The event-loop pops a task from the queue and invokes it (or gives +it control), similar to calling a function. That task then runs. Once it pauses or completes, it returns +control to the event-loop. The event-loop will then move on to the next task in its queue and invoke it. +This process repeats indefinitely. Even if the queue is empty, the event-loop continues to cycle (somewhat aimlessly). -Effective overall execution relies on tasks sharing well. A greedy task could hog control and leave the +Effective overall execution relies on tasks sharing well. A greedy task could hog control and leave the other tasks to starve rendering the overall event-loop approach rather useless. :: import asyncio - # This creates an event-loop and indefinitely cycles through + # This creates an event-loop and indefinitely cycles through # its queue of tasks. event_loop = asyncio.new_event_loop() event_loop.run_forever() @@ -78,7 +78,7 @@ This is a regular 'ol Python function:: 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 -- + Hi, I am a lowly, simple printer, though I have all I need in life -- fresh paper & a loving octopus-wife. >>> @@ -94,34 +94,34 @@ Calling an asynchronous function creates and returns a coroutine object. It does >>> special_fella(magic_number=3) - >>> + >>> -The terms "asynchronous function" (or "coroutine function") and "coroutine object" are often conflated -as coroutine. I find that a tad confusing. In this article, coroutine will exclusively mean "coroutine object" +The terms "asynchronous function" (or "coroutine function") and "coroutine object" are often conflated +as coroutine. I find that a tad confusing. In this article, coroutine will exclusively mean "coroutine object" -- the thing produced by executing a coroutine function. -That 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 & -resumed at various points within the function's body. That pausing & resuming ability is what allows +That 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 & +resumed at various points within the function's body. That pausing & resuming ability is what allows for asynchronous behavior! =========== Tasks =========== -Roughly speaking, tasks 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 ``await``. When tasks are created +Roughly speaking, tasks 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 ``await``. When tasks are created they are automatically added to the event-loop's queue of tasks:: # This creates a Task object and puts it on the event-loop's queue. special_task = asyncio.Task(coro=special_fella(magic_number=5), loop=event_loop) -It's common to see a task instantiated without explicitly specifying the event-loop it belongs to. Since -there's only one event-loop (a global singleton), asyncio made the loop argument optional and will add it +It's common to see a task instantiated without explicitly specifying the event-loop it belongs to. Since +there's only one event-loop (a global singleton), asyncio made the loop argument optional and will add it for you if it's left unspecified:: - # This creates another Task object and puts it on the event-loop's queue. - # The task is implicitly tied to the event-loop by asyncio since the + # This creates another Task object and puts it on the event-loop's queue. + # The task is implicitly tied to the event-loop by asyncio since the # loop argument was left unspecified. another_special_task = asyncio.Task(coro=special_fella(magic_number=12)) @@ -136,17 +136,17 @@ await Unfortunately, it actually does matter which type of object await is applied to. -``await``-ing a task will cede control from the current task or coroutine to the event-loop. And while doing so, -add a callback to the awaited task's list of callbacks indicating it should resume the current task/coroutine +``await``-ing a task will cede control from the current task or coroutine to the event-loop. And while doing so, +add a callback to the awaited task's list of callbacks indicating it should resume the current task/coroutine when it (the ``await``-ed one) finishes. Said another way, when that awaited task finishes, it adds the original task -back to the event-loops queue. +back to the event-loops queue. -In practice, it's slightly more convoluted, but not by much. In part 2, we'll walk through the -details that make this possible. And in the control flow analysis example we'll walk through, in precise detail, +In practice, it's slightly more convoluted, but not by much. In part 2, we'll walk through the +details that make this possible. And in the control flow analysis example we'll walk through, in precise detail, the various control handoffs in an example async program. -**Unlike tasks, await-ing a coroutine does not cede control!** Wrapping a coroutine in a task first, then ``await``-ing -that would cede control. The behavior of ``await coroutine`` is effectively the same as invoking a regular, +**Unlike tasks, await-ing a coroutine does not cede control!** Wrapping a coroutine in a task first, then ``await``-ing +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 @@ -178,7 +178,7 @@ of all three ``coro_a()`` invocations before ``coro_b()``'s output: I am coro_b(). I sure hope no one hogs the event-loop... If we change ``await coro_a()`` to ``await asyncio.Task(coro_a())``, the behavior changes. The coroutine -``main()`` cedes control to the event-loop with that statement. The event-loop then works through its queue, +``main()`` cedes control to the event-loop with that statement. The event-loop then works through its queue, calling ``coro_b()`` and then ``coro_a()`` before resuming the coroutine ``main()``. .. code-block:: none @@ -193,8 +193,8 @@ calling ``coro_b()`` and then ``coro_a()`` before resuming the coroutine ``main( A conceptual overview part 2: the nuts & bolts ---------------------------------------------- -Part 2 goes into detail on the mechanisms 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 +Part 2 goes into detail on the mechanisms 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. =============================================== @@ -203,17 +203,17 @@ coroutine.send(), await, yield & StopIteration asyncio leverages those 4 components to pass around control. -``coroutine.send(arg)`` is the method used to start or resume a coroutine. If the coroutine was paused and is now being +``coroutine.send(arg)`` 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 started, as opposed to resumed, ``arg`` must be None. -``yield``, like usual, pauses execution and returns control to the caller. In the example below, the ``yield`` is on line 3 -and the caller is ``... = await rock`` on line 11. Generally, ``await`` calls the ``__await__`` method of the given object. -``await`` also does one more very special thing: it percolates (or passes along) any yields it receives up the call-chain. +``yield``, like usual, pauses execution and returns control to the caller. In the example below, the ``yield`` is on line 3 +and the caller is ``... = await rock`` on line 11. Generally, ``await`` calls the ``__await__`` method of the given object. +``await`` also does one more very special thing: it percolates (or passes along) any yields 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 (i.e. paused) on line 3 and executes the remaining statements in its body. When a coroutine finishes +The coroutine is resumed via the ``coroutine.send(42)`` call on line 21. The coroutine picks back up from where it +``yield``-ed (i.e. paused) on line 3 and executes the remaining statements in its body. When a coroutine finishes it raises a ``StopIteration`` exception with the return value attached to the exception. :: @@ -223,19 +223,19 @@ it raises a ``StopIteration`` exception with the return value attached to the ex 3 value_sent_in = yield 7 4 print(f"Rock.__await__ resuming with value: {value_sent_in}.") 5 return value_sent_in - 6 + 6 7 async def main(): 8 print("Beginning coroutine main().") 9 rock = Rock() 10 print("Awaiting rock...") 11 value_from_rock = await rock - 12 print(f"Coroutine received value: {value_from_rock} from rock.") + 12 print(f"Coroutine received value: {value_from_rock} from rock.") 13 return 23 - 14 + 14 15 coroutine = main() 16 intermediate_result = coroutine.send(None) 17 print(f"Coroutine paused and returned intermediate value: {intermediate_result}.") - 18 + 18 19 print(f"Resuming coroutine and sending in value: 42.") 20 try: 21 coroutine.send(42) @@ -257,44 +257,44 @@ That snippet produces this output: It's worth pausing for a moment here and making sure you followed the various ways control flow and values were passed. -The only way to yield (or effectively cede control) from a coroutine is to ``await`` an object that ``yield``\ s in its ``__await__`` method. +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. Frankly, it was to me too. You might be thinking: - 1. What about a ``yield`` directly within the coroutine? The coroutine becomes + 1. What about a ``yield`` directly within the coroutine? The coroutine becomes a generator-coroutine, a different beast entirely. - 2. What about a ``yield from`` within the coroutine to a function that yields - (i.e. plain generator)? ``SyntaxError: yield from not allowed in a coroutine.`` - I imagine Python made this a ``SyntaxError`` to mandate only one way of using coroutines + 2. What about a ``yield from`` within the coroutine to a function that yields + (i.e. plain generator)? ``SyntaxError: yield from not allowed in a coroutine.`` + I imagine Python made this a ``SyntaxError`` to mandate only one way of using coroutines for the sake of simplicity. Ideologically, ``yield from`` and ``await`` are quite similar. =========== Futures =========== -A future is an object meant to represent a computation or process'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 +A future is an object meant to represent a computation or process'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. To be clear, a -future does not represent the actual computation to be done, like a coroutine does, instead it -represents the status and result of that computation, kind of like a status-light +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. To be clear, a +future does not represent the actual computation to be done, like a coroutine does, instead it +represents the status and result of that computation, kind of like a status-light (red, yellow or green) or indicator. -``Task`` subclasses ``Future`` in order to gain these various capabilities. I said in the prior section tasks store a list +``Task`` subclasses ``Future`` in order to gain these various capabilities. I said in the prior section tasks store a list of callbacks and I lied a bit. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. -Futures may be also used directly i.e. not via tasks. Tasks mark themselves as done when their coroutine's -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. +Futures may be also used directly i.e. not via tasks. Tasks mark themselves as done when their coroutine's +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. ========================== await-ing Tasks & futures ========================== -``Future`` defines an important method: ``__await__``. Below is the actual implementation (well, one line was -removed for simplicity's sake) found in ``asyncio.futures.Future``. It's okay if it doesn't make complete sense +``Future`` defines an important method: ``__await__``. Below is the actual implementation (well, one line was +removed for simplicity's sake) found in ``asyncio.futures.Future``. It's okay if it doesn't make complete sense now, we'll go through it in detail in the control-flow example. :: @@ -302,28 +302,28 @@ now, we'll go through it in detail in the control-flow example. 1 class Future: 2 ... 3 def __await__(self): - 4 + 4 5 if not self.done(): 6 yield self - 7 + 7 8 if not self.done(): 9 raise RuntimeError("await wasn't used with future") - 10 + 10 11 return self.result() -The ``Task`` class does not override ``Future``'s ``__await__`` implementation. ``await``-ing a task or future -invokes that above ``__await__`` method and percolates the ``yield`` on line 6 to relinquish +The ``Task`` class does not override ``Future``'s ``__await__`` implementation. ``await``-ing a task or future +invokes that above ``__await__`` method and percolates the ``yield`` on line 6 to relinquish control to its caller, which is generally the event-loop. ======================== 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 (i.e. asyncio.sleep). +We'll go through an example of how you could leverage a future to create your own variant of asynchronous +sleep (i.e. asyncio.sleep). -This snippet puts a few tasks on the event-loops queue and then ``await``\ s a -yet unknown coroutine wrapped in a task: ``async_sleep(3)``. We want that task to finish only after +This snippet puts a few tasks on the event-loops queue and then ``await``\ s a +yet unknown coroutine wrapped in a task: ``async_sleep(3)``. We want that task to finish only after 3 seconds have elapsed, but without hogging control while waiting. :: @@ -332,11 +332,11 @@ yet unknown coroutine wrapped in a task: ``async_sleep(3)``. We want that task t print(f"I am worker. Work work.") async def main(): - # Add a few other tasks to the event-loop, so there's something + # Add a few other tasks to the event-loop, so there's something # to do while asynchronously sleeping. work_tasks = [ - asyncio.Task(other_work()), - asyncio.Task(other_work()), + asyncio.Task(other_work()), + asyncio.Task(other_work()), asyncio.Task(other_work()) ] print( @@ -354,7 +354,7 @@ yet unknown coroutine wrapped in a task: ``async_sleep(3)``. We want that task t Below, we use a future to enable custom control over when that task will be marked as done. If ``future.set_result()``, the method responsible for marking that future as done, is never called, 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 +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()``. :: @@ -373,11 +373,11 @@ to cede control to the event-loop. This is effectively the same as calling ``asy but I prefer the clarity this approach offers, not to mention it's somewhat cheating to use ``asyncio.sleep`` when showcasing how to implement it! -The event-loop, as usual, cycles through its queue of tasks, giving them control, and receiving -control back when each task pauses or finishes. The ``watcher_task``, which runs the coroutine: ``_sleep_watcher(...)`` -will be invoked once per full cycle of the event-loop's queue. On each resumption, it'll check the time and -if not enough has elapsed, it'll pause once again and return control 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 +The event-loop, as usual, cycles through its queue of tasks, giving them control, and receiving +control back when each task pauses or finishes. The ``watcher_task``, which runs the coroutine: ``_sleep_watcher(...)`` +will be invoked once per full cycle of the event-loop's queue. On each resumption, it'll check the time and +if not enough has elapsed, it'll pause once again and return control 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's queue, 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 the library-provided asynchronous @@ -410,8 +410,8 @@ Here is the full program's output: Done asynchronous sleep at time: 14:52:25. You might feel this implementation of asynchronous sleep was unnecessarily convoluted. -And, well, it was. I wanted to showcase the versatility of futures with a -simple example that could be mimicked for more complex needs. For reference, +And, well, it was. I wanted 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): @@ -436,8 +436,8 @@ We'll walkthrough, step by step, a simple asynchronous program following along i Task.step =============== -The actual method that invokes a Tasks coroutine: ``asyncio.tasks.Task.__step_run_and_handle_result`` -is about 80 lines long. For the sake of clarity, I've removed all of the edge-case error handling, +The actual method that invokes a Tasks coroutine: ``asyncio.tasks.Task.__step_run_and_handle_result`` +is about 80 lines long. For the sake of clarity, I've removed all of the edge-case error handling, simplified some aspects and renamed it, but the core logic remains unchanged. :: @@ -501,51 +501,51 @@ At a high-level, this is how control flows: And, in much more detail: -1. Control begins in ``program.py`` - Line 9 creates an event-loop, line 10 creates ``main_task`` and adds it to +1. Control begins in ``program.py`` + Line 9 creates an event-loop, line 10 creates ``main_task`` and adds it to the event-loop, line 11 indefinitely passes control to the event-loop. 2. Control is now in the event-loop The event-loop pops ``main_task`` off its queue, then invokes it by calling ``main_task.step()``. 3. Control is now in ``main_task.step`` We enter the try-block on line 4 then begin the coroutine ``main()`` on line 5. 4. Control is now in the coroutine: ``main()`` - The Task ``triple_task`` is created on line 5 which adds it to the event-loops queue. Line 6 + The Task ``triple_task`` is created on line 5 which adds it to the event-loops queue. Line 6 ``await``\ s triple_task. Remember, that calls ``Task.__await__`` then percolates any ``yield``\ s. 5. Control is now in ``triple_task.__await__`` - ``triple_task`` is not done given it was just created, so we enter the first if-block on line 5 and ``yield`` the thing + ``triple_task`` is not done given it was just created, so we enter the first if-block on line 5 and ``yield`` the thing we'll be waiting for -- ``triple_task``. 6. Control is now in the coroutine: ``main()`` ``await`` percolates the ``yield`` and the yielded value -- ``triple_task``. 7. Control is now in ``main_task.step`` - The variable ``awaited_task`` is ``triple_task``. No ``StopIteration`` was raised so the else in the try-block - on line 8 executes. A done-callback: ``main_task.step`` is added to the ``triple_task``. The step method ends and + The variable ``awaited_task`` is ``triple_task``. No ``StopIteration`` was raised so the else in the try-block + on line 8 executes. A done-callback: ``main_task.step`` is added to the ``triple_task``. The step method ends and returns to the event-loop. 8. Control is now in the event-loop - The event-loop cycles to the next task in its queue. The event-loop pops ``triple_task`` from its queue and invokes + The event-loop cycles to the next task in its queue. The event-loop pops ``triple_task`` from its queue and invokes it by calling ``triple_task.step()``. 9. Control is now in ``triple_task.step`` We enter the try-block on line 4 then begin the coroutine ``triple()`` via line 5. 10. Control is now in the coroutine: ``triple()`` It computes 3 times 5, then finishes and raises a ``StopIteration`` exception. 11. Control is now in ``triple_task.step`` - The ``StopIteration`` exception is caught so we go to line 7. The return value of the coroutine ``triple()`` is embedded in the - value attribute of that exception. ``Future.set_result()`` saves the result, marks the task as done and adds - the done-callbacks of ``triple_task`` to the event-loops queue. The step method ends and returns control to the + The ``StopIteration`` exception is caught so we go to line 7. The return value of the coroutine ``triple()`` is embedded in the + value attribute of that exception. ``Future.set_result()`` saves the result, marks the task as done and adds + the done-callbacks of ``triple_task`` to the event-loops queue. The step method ends and returns control to the event-loop. 12. Control is now in the event-loop - The event-loop cycles to the next task in its queue. The event-loop pops ``main_task`` and resumes it by + The event-loop cycles to the next task in its queue. The event-loop pops ``main_task`` and resumes it by calling ``main_task.step()``. 13. Control is now in ``main_task.step`` - We enter the try-block on line 4 then resume the coroutine ``main`` which will pick up again from where it + We enter the try-block on line 4 then resume the coroutine ``main`` which will pick up again from where it ``yield``-ed. Recall, it ``yield``-ed not in the coroutine, but in ``triple_task.__await__`` on line 6. 14. Control is now in ``triple_task.__await__`` - We evaluate the if-statement on line 8 which ensures that ``triple_task`` was completed. Then, it returns the - result of ``triple_task`` which was saved earlier. Finally that result is returned to the + We evaluate the if-statement on line 8 which ensures that ``triple_task`` was completed. Then, it returns the + result of ``triple_task`` which was saved earlier. Finally that result is returned to the caller (i.e. ``... = await triple_task``). 15. Control is now in the coroutine: ``main()`` ``tripled_val`` is 15. The coroutine finishes and raises a ``StopIteration`` exception with the return value of 17 attached. 16. Control is now in ``main_task.step`` - The ``StopIteration`` exception is caught and ``main_task`` is marked as done and its result is saved. The step method ends + The ``StopIteration`` exception is caught and ``main_task`` is marked as done and its result is saved. The step method ends and returns control to the event-loop. 17. Control is now in the event-loop There's nothing in the queue. The event-loop cycles aimlessly onwards. @@ -554,23 +554,23 @@ And, in much more detail: Barebones network I/O example ---------------------------------------------- -Here we'll see a simple but thorough example showing how asyncio can offer an advantage over serial programs. The example doesn't rely on any asyncio -operators (besides the event-loop). It's all non-blocking sockets & custom awaitables that help you see what's actually happening under the +Here we'll see a simple but thorough example showing how asyncio can offer an advantage over serial programs. The example doesn't rely on any asyncio +operators (besides the event-loop). It's all non-blocking sockets & custom awaitables that help you see what's actually happening under the hood and how you could do something similar. -Performing a database request across a network might take half a second or so, but that's ages in computer-time. Your processor could have -done millions or even billions of things. The same is true for, say, requesting a website, downloading a car, loading a file from disk +Performing a database request across a network might take half a second or so, but that's ages in computer-time. Your processor could have +done millions or even billions of things. The same is true for, say, requesting a website, downloading a car, loading a file from disk into memory, etc. The general theme is those are all input/output (I/O) actions. -Consider performing two tasks: requesting some information from a server and doing some computation locally. A serial approach would -look like: ping the server, idle while waiting for a response, receive the response, perform the local computation. An asynchronous -approach would look like: ping the server, do some of the local computation while waiting for a response, check if the server is -ready yet, do a bit more of the local computation, check again, etc. Basically we're freeing up the CPU to do other activities +Consider performing two tasks: requesting some information from a server and doing some computation locally. A serial approach would +look like: ping the server, idle while waiting for a response, receive the response, perform the local computation. An asynchronous +approach would look like: ping the server, do some of the local computation while waiting for a response, check if the server is +ready yet, do a bit more of the local computation, check again, etc. Basically we're freeing up the CPU to do other activities instead of scratching its belly button. -This example has a server (a separate, local process) compute the sum of many samples from a Gaussian (i.e. normal) distribution. -And the local computation finds the sum of many samples from a uniform distribution. As you'll see, the asynchronous approach -runs notably faster, since progress can be made on computing the sum of many uniform samples, while waiting for the server to +This example has a server (a separate, local process) compute the sum of many samples from a Gaussian (i.e. normal) distribution. +And the local computation finds the sum of many samples from a uniform distribution. As you'll see, the asynchronous approach +runs notably faster, since progress can be made on computing the sum of many uniform samples, while waiting for the server to calculate and respond. ===================== @@ -626,30 +626,30 @@ Asynchronous output Code ====================== -Now, we'll explore some of the most important snippets. +Now, we'll explore some of the most important snippets. Below is the portion of the asynchronous approach responsible for checking if the server's done yet. And, if not, yielding control back to the event-loop instead of idly waiting. I'd like to draw your attention to a specific part of this snippet. Setting a socket to non-blocking mode means the ``recv()`` call -won't idle while waiting for a response. Instead, if there's no data to be read, it'll immediately -raise a ``BlockingIOError``. If there is data available, the ``recv()`` will proceed as normal. +won't idle while waiting for a response. Instead, if there's no data to be read, it'll immediately +raise a ``BlockingIOError``. If there is data available, the ``recv()`` will proceed as normal. .. code-block:: python - + class YieldToEventLoop: def __await__(self): yield ... - + async def get_server_data(): client = socket.socket() client.connect(server.SERVER_ADDRESS) client.setblocking(False) - + while True: try: - # For reference, the first argument to recv() is the maximum number - # of bytes to attempt to read. Setting it to 4096 means we could get 2 + # For reference, the first argument to recv() is the maximum number + # of bytes to attempt to read. Setting it to 4096 means we could get 2 # bytes or 4 bytes, or even 4091 bytes, but not 4097 bytes back. # However, if there are no bytes available to be read, this recv() # will raise a BlockingIOError since the socket was set to @@ -663,14 +663,14 @@ raise a ``BlockingIOError``. If there is data available, the ``recv()`` will pro And this is the portion of code responsible for asynchronously computing the uniform sums. It's designed to allow for working through the sum a portion at a time. -The ``time_allotment`` argument to the coroutine function decides how long the sum +The ``time_allotment`` argument to the coroutine function decides how long the sum function will iterate, in other words, synchronously hog control, before ceding -back to the event-loop. +back to the event-loop. .. code-block:: python async def uniform_sum(n_samples: int, time_allotment: float) -> float: - + start_time = time.time() total = 0.0 @@ -681,18 +681,18 @@ back to the event-loop. if time_elapsed > time_allotment: await YieldToEventLoop() start_time = time.time() - + return total The above snippet was simplified a bit. Reading ``time.time()`` and evaluating an if-condition on every iteration for many, many iterations (in this case roughly a hundred million) more than eats up the runtime savings associated with the asynchronous approach. The actual -implementation involves chunking the iteration, so you only perform the check -every few million iterations. With that change, the asynchronous approach wins in a landslide. -This is important to keep in mind. Too much checking or constantly jumping between tasks can +implementation involves chunking the iteration, so you only perform the check +every few million iterations. With that change, the asynchronous approach wins in a landslide. +This is important to keep in mind. Too much checking or constantly jumping between tasks can ultimately cause more harm than good! -The server, async & serial programs are available in full here: +The server, async & serial programs are available in full here: https://github.com/anordin95/a-conceptual-overview-of-asyncio/tree/main/barebones-network-io-example. .. _which_concurrency_do_I_want: @@ -705,32 +705,32 @@ Which concurrency do I want multiprocessing =========================== -For any computationally bound work in Python, you likely want to use multiprocessing. -Otherwise, the Global Interpreter Lock (GIL) will generally get in your way! For those who don't -know, the GIL is a lock which ensures only one Python instruction is executed at a time. -Of course, since processes are generally entirely independent from one another, the GIL in one -process won't be impeded by the GIL in another process. Granted, I believe there are ways to +For any computationally bound work in Python, you likely want to use multiprocessing. +Otherwise, the Global Interpreter Lock (GIL) will generally get in your way! For those who don't +know, the GIL is a lock which ensures only one Python instruction is executed at a time. +Of course, since processes are generally entirely independent from one another, the GIL in one +process won't be impeded by the GIL in another process. Granted, I believe there are ways to also get around the GIL in a single process by leveraging C extensions. =========================== multithreading & asyncio =========================== -Multithreading and asyncio are much more similar in where they're useful with Python: not at all for -computationally-bound work, and crucially for I/O bound work. For applications that need to +Multithreading and asyncio are much more similar in where they're useful with Python: not at all for +computationally-bound work, and crucially for I/O bound work. For applications that need to manage absolutely tons of distinct I/O connections or chunks-of-work, asyncio is a must. For example, a web server handling thousands of requests "simultaneously" (in quotes, because, as we saw, the frequent -handoffs of control only create the illusion of simultaneous execution). Otherwise, I think the choice +handoffs of control only create the illusion of simultaneous execution). Otherwise, I think the choice between which to use is somewhat down to taste. -Multithreading maintains an OS managed thread for each chunk of work. Whereas asyncio uses Tasks for -each work-chunk and manages them via the event-loop's queue. I believe the marginal overhead of one more -chunk of work is a fair bit lower for asyncio than threads, which matters a lot for applications that +Multithreading maintains an OS managed thread for each chunk of work. Whereas asyncio uses Tasks for +each work-chunk and manages them via the event-loop's queue. I believe the marginal overhead of one more +chunk of work is a fair bit lower for asyncio than threads, which matters a lot for applications that need to manage many, many chunks of work. -There are some other benefits associated with using asyncio. One is clearer visibility into when and where -interleaving occurs. The code chunk between two awaits is certainly synchronous. Another is simpler debugging, -since it's easier to attach and follow a trace and reason about code execution. With threading, the interleaving -is more of a black-box. One benefit of multithreading is not really having to worry about greedy threads -hogging execution, something that could happen with asyncio where a greedy coroutine never awaits and +There are some other benefits associated with using asyncio. One is clearer visibility into when and where +interleaving occurs. The code chunk between two awaits is certainly synchronous. Another is simpler debugging, +since it's easier to attach and follow a trace and reason about code execution. With threading, the interleaving +is more of a black-box. One benefit of multithreading is not really having to worry about greedy threads +hogging execution, something that could happen with asyncio where a greedy coroutine never awaits and effectively stalls the event-loop. From 176096d5aa6a5256245a8caf175726d2740e9ddd Mon Sep 17 00:00:00 2001 From: anordin95 Date: Tue, 29 Jul 2025 12:49:19 -0700 Subject: [PATCH 03/70] - Enforce max line length of roughly 79 chars. - Start sentences on new lines to minimize disruption of diffs. --- .../a-conceptual-overview-of-asyncio.rst | 534 +++++++++++------- Doc/library/asyncio.rst | 4 +- 2 files changed, 329 insertions(+), 209 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index ca40e2e59d1b9f..bba4e911b0bafe 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -7,21 +7,30 @@ A Conceptual Overview of asyncio :Author: Alexander Nordin This article seeks to help you build a sturdy mental model of how asyncio -fundamentally works. Something that will help you understand the how and why -behind the recommended patterns. The final section, :ref:`which_concurrency_do_I_want`, zooms out a bit and compares the -common approaches to concurrency -- multiprocessing, multithreading & asyncio -- and describes where -each is most useful. - -During my own asyncio learning process, a few aspects particually drove my curiosity (read: drove me nuts). You -should be able to comfortably answer all these questions by the end of this article. +fundamentally works. +Something that will help you understand the how and why behind the recommended +patterns. +The final section, :ref:`which_concurrency_do_I_want`, zooms out a bit and +compares the common approaches to concurrency -- multiprocessing, +multithreading & asyncio -- and describes where each is most useful. + +During my own asyncio learning process, a few aspects particually drove my +curiosity (read: drove me nuts). +You should be able to comfortably answer all these questions by the end +of this article. - What's roughly happening behind the scenes when an object is ``await``-ed? -- How does asyncio differentiate between a task which doesn't need CPU-time to make progress towards completion, for example, a network request or file read as opposed to a task that does need cpu-time to make progress, like computing n-factorial? -- How would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, etc. - -The first two sections feature some examples but are generally focused on theory and explaining concepts. -The next two sections are centered around examples, focused on further illustrating and reinforcing ideas -practically. +- How does asyncio differentiate between a task which doesn't need CPU-time + to make progress towards completion, for example, a network request or file + read as opposed to a task that does need cpu-time to make progress, like + computing n-factorial? +- How would I go about writing my own asynchronous variant of some operation? + Something like an async sleep, database request, etc. + +The first two sections feature some examples but are generally focused on theory +and explaining concepts. +The next two sections are centered around examples, focused on further +illustrating and reinforcing ideas practically. .. contents:: Sections :depth: 1 @@ -31,28 +40,34 @@ practically. A conceptual overview part 1: the high-level --------------------------------------------- -In part 1, we'll cover the main, high-level building blocks of asyncio: the event-loop, -coroutine functions, coroutine objects, tasks & ``await``. +In part 1, we'll cover the main, high-level building blocks of asyncio: the +event-loop, coroutine functions, coroutine objects, tasks & ``await``. ========================== Event Loop ========================== -Everything in asyncio happens relative to the event-loop. It's the star of the show and there's only one. -It's kind of like an orchestra conductor or military general. She's behind the scenes managing resources. -Some power is explicitly granted to her, but a lot of her ability to get things done comes from the respect -& cooperation of her subordinates. - -In more technical terms, the event-loop contains a queue of tasks to be run. Some tasks are added directly -by you, and some indirectly by asyncio. The event-loop pops a task from the queue and invokes it (or gives -it control), similar to calling a function. That task then runs. Once it pauses or completes, it returns -control to the event-loop. The event-loop will then move on to the next task in its queue and invoke it. -This process repeats indefinitely. Even if the queue is empty, the event-loop continues to cycle -(somewhat aimlessly). - -Effective overall execution relies on tasks sharing well. A greedy task could hog control and leave the -other tasks to starve rendering the overall event-loop approach rather useless. +Everything in asyncio happens relative to the event-loop. +It's the star of the show and there's only one. +It's kind of like an orchestra conductor or military general. +She's behind the scenes managing resources. +Some power is explicitly granted to her, but a lot of her ability to get things +done comes from the respect & cooperation of her subordinates. + +In more technical terms, the event-loop contains a queue of tasks to be run. +Some tasks are added directly by you, and some indirectly by asyncio. +The event-loop pops a task from the queue and invokes it (or gives it control), +similar to calling a function. +That task then runs. +Once it pauses or completes, it returns control to the event-loop. +The event-loop will then move on to the next task in its queue and invoke it. +This process repeats indefinitely. +Even if the queue is empty, the event-loop continues to cycle (somewhat aimlessly). + +Effective overall execution relies on tasks sharing well. +A greedy task could hog control and leave the other tasks to starve rendering +the overall event-loop approach rather useless. :: @@ -90,35 +105,44 @@ This is an asynchronous-function or coroutine-function:: f"By the way, my lucky number is: {magic_number}." ) -Calling an asynchronous function creates and returns a coroutine object. It does not execute the function:: +Calling an asynchronous function creates and returns a coroutine object. It +does not execute the function:: >>> special_fella(magic_number=3) >>> -The terms "asynchronous function" (or "coroutine function") and "coroutine object" are often conflated -as coroutine. I find that a tad confusing. In this article, coroutine will exclusively mean "coroutine object" --- the thing produced by executing a coroutine function. +The terms "asynchronous function" (or "coroutine function") and "coroutine object" +are often conflated as coroutine. +I find that a tad confusing. +In this article, coroutine will exclusively mean "coroutine object" -- the +thing produced by executing a coroutine function. -That 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 & -resumed at various points within the function's body. That pausing & resuming ability is what allows -for asynchronous behavior! +That 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 & resumed at various points within the +function's body. +That pausing & resuming ability is what allows for asynchronous behavior! =========== Tasks =========== -Roughly speaking, tasks 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 ``await``. When tasks are created -they are automatically added to the event-loop's queue of tasks:: +Roughly speaking, tasks 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 ``await``. +When tasks are created they are automatically added to the event-loop's queue +of tasks:: # This creates a Task object and puts it on the event-loop's queue. special_task = asyncio.Task(coro=special_fella(magic_number=5), loop=event_loop) -It's common to see a task instantiated without explicitly specifying the event-loop it belongs to. Since -there's only one event-loop (a global singleton), asyncio made the loop argument optional and will add it -for you if it's left unspecified:: +It's common to see a task instantiated without explicitly specifying the event-loop +it belongs to. +Since there's only one event-loop (a global singleton), asyncio made the loop +argument optional and will add it for you if it's left unspecified:: # This creates another Task object and puts it on the event-loop's queue. # The task is implicitly tied to the event-loop by asyncio since the @@ -136,18 +160,24 @@ await Unfortunately, it actually does matter which type of object await is applied to. -``await``-ing a task will cede control from the current task or coroutine to the event-loop. And while doing so, -add a callback to the awaited task's list of callbacks indicating it should resume the current task/coroutine -when it (the ``await``-ed one) finishes. Said another way, when that awaited task finishes, it adds the original task +``await``-ing a task will cede control from the current task or coroutine to +the event-loop. +And while doing so, add a callback to the awaited task's list of callbacks +indicating it should resume the current task/coroutine when it (the +``await``-ed one) finishes. +Said another way, when that awaited task finishes, it adds the original task back to the event-loops queue. -In practice, it's slightly more convoluted, but not by much. In part 2, we'll walk through the -details that make this possible. And in the control flow analysis example we'll walk through, in precise detail, +In practice, it's slightly more convoluted, but not by much. +In part 2, we'll walk through the details that make this possible. +And in the control flow analysis example we'll walk through, in precise detail, the various control handoffs in an example async program. -**Unlike tasks, await-ing a coroutine does not cede control!** Wrapping a coroutine in a task first, then ``await``-ing -that would cede control. The behavior of ``await coroutine`` is effectively the same as invoking a regular, -synchronous Python function. Consider this program:: +**Unlike tasks, await-ing a coroutine does not cede control!** +Wrapping a coroutine in a task first, then ``await``-ing 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 @@ -166,9 +196,11 @@ synchronous Python function. Consider this program:: asyncio.run(main()) -The first statement in the coroutine ``main()`` creates ``task_b`` and places it on the event-loops queue. Then, -``coro_a()`` is repeatedly ``await``-ed. 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: +The first statement in the coroutine ``main()`` creates ``task_b`` and places +it on the event-loops queue. +Then, ``coro_a()`` is repeatedly ``await``-ed. 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 @@ -177,9 +209,11 @@ of all three ``coro_a()`` invocations before ``coro_b()``'s output: 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.Task(coro_a())``, the behavior changes. The coroutine -``main()`` cedes control to the event-loop with that statement. The event-loop then works through its queue, -calling ``coro_b()`` and then ``coro_a()`` before resuming the coroutine ``main()``. +If we change ``await coro_a()`` to ``await asyncio.Task(coro_a())``, the +behavior changes. +The coroutine ``main()`` cedes control to the event-loop with that statement. +The event-loop then works through its queue, calling ``coro_b()`` and then +``coro_a()`` before resuming the coroutine ``main()``. .. code-block:: none @@ -193,9 +227,10 @@ calling ``coro_b()`` and then ``coro_a()`` before resuming the coroutine ``main( A conceptual overview part 2: the nuts & bolts ---------------------------------------------- -Part 2 goes into detail on the mechanisms 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. +Part 2 goes into detail on the mechanisms 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. =============================================== coroutine.send(), await, yield & StopIteration @@ -203,18 +238,25 @@ coroutine.send(), await, yield & StopIteration asyncio leverages those 4 components to pass around control. -``coroutine.send(arg)`` 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 started, as opposed to resumed, ``arg`` must be None. - -``yield``, like usual, pauses execution and returns control to the caller. In the example below, the ``yield`` is on line 3 -and the caller is ``... = await rock`` on line 11. Generally, ``await`` calls the ``__await__`` method of the given object. -``await`` also does one more very special thing: it percolates (or passes along) any yields it receives up the call-chain. +``coroutine.send(arg)`` 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 started, as opposed to resumed, ``arg`` must be None. + +``yield``, like usual, pauses execution and returns control to the caller. +In the example below, the ``yield`` is on line 3 and the caller is +``... = await rock`` on line 11. +Generally, ``await`` calls the ``__await__`` method of the given object. +``await`` also does one more very special thing: it percolates (or passes along) +any yields 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 (i.e. paused) on line 3 and executes the remaining statements in its body. When a coroutine finishes -it raises a ``StopIteration`` exception with the return value attached to the exception. +The coroutine is resumed via the ``coroutine.send(42)`` call on line 21. +The coroutine picks back up from where it ``yield``-ed (i.e. paused) on line 3 +and executes the remaining statements in its body. +When a coroutine finishes it raises a ``StopIteration`` exception with the +return value attached to the exception. :: @@ -255,47 +297,59 @@ That snippet produces this output: 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 control flow and values were passed. +It's worth pausing for a moment here and making sure you followed the various +ways control flow and values were passed. -The only way to yield (or effectively cede control) from a coroutine is to ``await`` an object that ``yield``\ s in its ``__await__`` method. +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. Frankly, it was to me too. You might be thinking: 1. What about a ``yield`` directly within the coroutine? The coroutine becomes a generator-coroutine, a different beast entirely. 2. What about a ``yield from`` within the coroutine to a function that yields - (i.e. plain generator)? ``SyntaxError: yield from not allowed in a coroutine.`` - I imagine Python made this a ``SyntaxError`` to mandate only one way of using coroutines - for the sake of simplicity. Ideologically, ``yield from`` and ``await`` are quite similar. + (i.e. plain generator)? + ``SyntaxError: yield from not allowed in a coroutine.`` + I imagine Python made this a ``SyntaxError`` to mandate only one way of using + coroutines for the sake of simplicity. + Ideologically, ``yield from`` and ``await`` are quite similar. =========== Futures =========== -A future is an object meant to represent a computation or process'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. To be clear, a -future does not represent the actual computation to be done, like a coroutine does, instead it -represents the status and result of that computation, kind of like a status-light -(red, yellow or green) or indicator. - -``Task`` subclasses ``Future`` in order to gain these various capabilities. I said in the prior section tasks store a list -of callbacks and I lied a bit. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. - -Futures may be also used directly i.e. not via tasks. Tasks mark themselves as done when their coroutine's -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 future is an object meant to represent a computation or process'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. +To be clear, a future does not represent the actual computation to be done, like +a coroutine does, instead it represents the status and result of that computation, +kind of like a status-light (red, yellow or green) or indicator. + +``Task`` subclasses ``Future`` in order to gain these various capabilities. +I said in the prior section tasks store a list of callbacks and I lied a bit. +It's actually the ``Future`` class that implements this logic which ``Task`` +inherits. + +Futures may be also used directly i.e. not via tasks. +Tasks mark themselves as done when their coroutine's 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. ========================== await-ing Tasks & futures ========================== -``Future`` defines an important method: ``__await__``. Below is the actual implementation (well, one line was -removed for simplicity's sake) found in ``asyncio.futures.Future``. It's okay if it doesn't make complete sense -now, we'll go through it in detail in the control-flow example. +``Future`` defines an important method: ``__await__``. Below is the actual +implementation (well, one line was removed for simplicity's sake) found +in ``asyncio.futures.Future``. +It's okay if it doesn't make complete sense now, we'll go through it in detail +in the control-flow example. :: @@ -311,20 +365,22 @@ now, we'll go through it in detail in the control-flow example. 10 11 return self.result() -The ``Task`` class does not override ``Future``'s ``__await__`` implementation. ``await``-ing a task or future -invokes that above ``__await__`` method and percolates the ``yield`` on line 6 to relinquish -control to its caller, which is generally the event-loop. +The ``Task`` class does not override ``Future``'s ``__await__`` implementation. +``await``-ing a task or future invokes that above ``__await__`` method and +percolates the ``yield`` on line 6 to relinquish control to its caller, which +is generally the event-loop. ======================== 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 (i.e. asyncio.sleep). +We'll go through an example of how you could leverage a future to create your +own variant of asynchronous sleep (i.e. asyncio.sleep). This snippet puts a few tasks on the event-loops queue and then ``await``\ s a -yet unknown coroutine wrapped in a task: ``async_sleep(3)``. We want that task to finish only after -3 seconds have elapsed, but without hogging control while waiting. +yet unknown coroutine wrapped in a task: ``async_sleep(3)``. +We want that task to finish only after 3 seconds have elapsed, but without +hogging control while waiting. :: @@ -352,10 +408,13 @@ yet unknown coroutine wrapped in a task: ``async_sleep(3)``. We want that task t await asyncio.gather(*work_tasks) -Below, we use a future to enable custom control over when that task will be marked as done. If ``future.set_result()``, -the method responsible for marking that future as done, is never called, 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()``. +Below, we use a future to enable custom control over when that task will be marked +as done. +If ``future.set_result()``, the method responsible for marking that future as +done, is never called, 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()``. :: @@ -368,20 +427,26 @@ accordingly call ``future.set_result()``. await future -We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its ``__await__`` in order -to cede control to the event-loop. This is effectively the same as calling ``asyncio.sleep(0)``, -but I prefer the clarity this approach offers, not to mention it's somewhat cheating to use +We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its +``__await__`` in order to cede control to the event-loop. +This is effectively the same as calling ``asyncio.sleep(0)``, but I prefer the +clarity this approach offers, not to mention it's somewhat cheating to use ``asyncio.sleep`` when showcasing how to implement it! -The event-loop, as usual, cycles through its queue of tasks, giving them control, and receiving -control back when each task pauses or finishes. The ``watcher_task``, which runs the coroutine: ``_sleep_watcher(...)`` -will be invoked once per full cycle of the event-loop's queue. On each resumption, it'll check the time and -if not enough has elapsed, it'll pause once again and return control 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's -queue, 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 the library-provided asynchronous -function: ``asyncio.sleep``. +The event-loop, as usual, cycles through its queue of tasks, giving them control, +and receiving control back when each task pauses or finishes. +The ``watcher_task``, which runs the coroutine: ``_sleep_watcher(...)`` will be +invoked once per full cycle of the event-loop's queue. +On each resumption, it'll check the time and if not enough has elapsed, it'll +pause once again and return control 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's queue, +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 the library-provided asynchronous function: +``asyncio.sleep``. :: @@ -409,10 +474,12 @@ Here is the full program's output: I am worker. 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. I wanted 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:: +You might feel this implementation of asynchronous sleep was unnecessarily +convoluted. +And, well, it was. +I wanted 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 @@ -429,15 +496,18 @@ you could implement it without futures, like so:: Analyzing an example programs control flow ---------------------------------------------- -We'll walkthrough, step by step, a simple asynchronous program following along in the key methods of Task & Future that are leveraged when asyncio is orchestrating the show. +We'll walkthrough, step by step, a simple asynchronous program following along +in the key methods of Task & Future that are leveraged when asyncio is +orchestrating the show. =============== Task.step =============== -The actual method that invokes a Tasks coroutine: ``asyncio.tasks.Task.__step_run_and_handle_result`` -is about 80 lines long. For the sake of clarity, I've removed all of the edge-case error handling, +The actual method that invokes a Tasks coroutine: +``asyncio.tasks.Task.__step_run_and_handle_result`` is about 80 lines long. +For the sake of clarity, I've removed all of the edge-case error handling, simplified some aspects and renamed it, but the core logic remains unchanged. :: @@ -505,73 +575,104 @@ And, in much more detail: Line 9 creates an event-loop, line 10 creates ``main_task`` and adds it to the event-loop, line 11 indefinitely passes control to the event-loop. 2. Control is now in the event-loop - The event-loop pops ``main_task`` off its queue, then invokes it by calling ``main_task.step()``. + The event-loop pops ``main_task`` off its queue, then invokes it by calling + ``main_task.step()``. 3. Control is now in ``main_task.step`` - We enter the try-block on line 4 then begin the coroutine ``main()`` on line 5. + We enter the try-block on line 4 then begin the coroutine ``main()`` on + line 5. 4. Control is now in the coroutine: ``main()`` - The Task ``triple_task`` is created on line 5 which adds it to the event-loops queue. Line 6 - ``await``\ s triple_task. Remember, that calls ``Task.__await__`` then percolates any ``yield``\ s. + The Task ``triple_task`` is created on line 5 which adds it to the + event-loops queue. Line 6 ``await``\ s triple_task. + Remember, that calls ``Task.__await__`` then percolates any ``yield``\ s. 5. Control is now in ``triple_task.__await__`` - ``triple_task`` is not done given it was just created, so we enter the first if-block on line 5 and ``yield`` the thing - we'll be waiting for -- ``triple_task``. + ``triple_task`` is not done given it was just created, so we enter + the first if-block on line 5 and ``yield`` the thing we'll be waiting + for -- ``triple_task``. 6. Control is now in the coroutine: ``main()`` ``await`` percolates the ``yield`` and the yielded value -- ``triple_task``. 7. Control is now in ``main_task.step`` - The variable ``awaited_task`` is ``triple_task``. No ``StopIteration`` was raised so the else in the try-block - on line 8 executes. A done-callback: ``main_task.step`` is added to the ``triple_task``. The step method ends and - returns to the event-loop. + The variable ``awaited_task`` is ``triple_task``. + No ``StopIteration`` was raised so the else in the try-block on line 8 + executes. + A done-callback: ``main_task.step`` is added to the ``triple_task``. + The step method ends and returns to the event-loop. 8. Control is now in the event-loop - The event-loop cycles to the next task in its queue. The event-loop pops ``triple_task`` from its queue and invokes - it by calling ``triple_task.step()``. + The event-loop cycles to the next task in its queue. + The event-loop pops ``triple_task`` from its queue and invokes it by + calling ``triple_task.step()``. 9. Control is now in ``triple_task.step`` - We enter the try-block on line 4 then begin the coroutine ``triple()`` via line 5. + We enter the try-block on line 4 then begin the coroutine ``triple()`` + via line 5. 10. Control is now in the coroutine: ``triple()`` - It computes 3 times 5, then finishes and raises a ``StopIteration`` exception. + It computes 3 times 5, then finishes and raises a ``StopIteration`` + exception. 11. Control is now in ``triple_task.step`` - The ``StopIteration`` exception is caught so we go to line 7. The return value of the coroutine ``triple()`` is embedded in the - value attribute of that exception. ``Future.set_result()`` saves the result, marks the task as done and adds - the done-callbacks of ``triple_task`` to the event-loops queue. The step method ends and returns control to the - event-loop. + The ``StopIteration`` exception is caught so we go to line 7. + The return value of the coroutine ``triple()`` is embedded in the value + attribute of that exception. + ``Future.set_result()`` saves the result, marks the task as done and adds + the done-callbacks of ``triple_task`` to the event-loops queue. + The step method ends and returns control to the event-loop. 12. Control is now in the event-loop - The event-loop cycles to the next task in its queue. The event-loop pops ``main_task`` and resumes it by - calling ``main_task.step()``. + The event-loop cycles to the next task in its queue. + The event-loop pops ``main_task`` and resumes it by calling + ``main_task.step()``. 13. Control is now in ``main_task.step`` - We enter the try-block on line 4 then resume the coroutine ``main`` which will pick up again from where it - ``yield``-ed. Recall, it ``yield``-ed not in the coroutine, but in ``triple_task.__await__`` on line 6. + We enter the try-block on line 4 then resume the coroutine ``main`` + which will pick up again from where it ``yield``-ed. + Recall, it ``yield``-ed not in the coroutine, but in + ``triple_task.__await__`` on line 6. 14. Control is now in ``triple_task.__await__`` - We evaluate the if-statement on line 8 which ensures that ``triple_task`` was completed. Then, it returns the - result of ``triple_task`` which was saved earlier. Finally that result is returned to the - caller (i.e. ``... = await triple_task``). + We evaluate the if-statement on line 8 which ensures that ``triple_task`` + was completed. + Then, it returns the result of ``triple_task`` which was saved earlier. + Finally that result is returned to the caller + (i.e. ``... = await triple_task``). 15. Control is now in the coroutine: ``main()`` - ``tripled_val`` is 15. The coroutine finishes and raises a ``StopIteration`` exception with the return value of 17 attached. + ``tripled_val`` is 15. The coroutine finishes and raises a + ``StopIteration`` exception with the return value of 17 attached. 16. Control is now in ``main_task.step`` - The ``StopIteration`` exception is caught and ``main_task`` is marked as done and its result is saved. The step method ends - and returns control to the event-loop. + The ``StopIteration`` exception is caught and ``main_task`` is marked + as done and its result is saved. + The step method ends and returns control to the event-loop. 17. Control is now in the event-loop - There's nothing in the queue. The event-loop cycles aimlessly onwards. + There's nothing in the queue. + The event-loop cycles aimlessly onwards. ---------------------------------------------- Barebones network I/O example ---------------------------------------------- -Here we'll see a simple but thorough example showing how asyncio can offer an advantage over serial programs. The example doesn't rely on any asyncio -operators (besides the event-loop). It's all non-blocking sockets & custom awaitables that help you see what's actually happening under the -hood and how you could do something similar. - -Performing a database request across a network might take half a second or so, but that's ages in computer-time. Your processor could have -done millions or even billions of things. The same is true for, say, requesting a website, downloading a car, loading a file from disk -into memory, etc. The general theme is those are all input/output (I/O) actions. - -Consider performing two tasks: requesting some information from a server and doing some computation locally. A serial approach would -look like: ping the server, idle while waiting for a response, receive the response, perform the local computation. An asynchronous -approach would look like: ping the server, do some of the local computation while waiting for a response, check if the server is -ready yet, do a bit more of the local computation, check again, etc. Basically we're freeing up the CPU to do other activities -instead of scratching its belly button. - -This example has a server (a separate, local process) compute the sum of many samples from a Gaussian (i.e. normal) distribution. -And the local computation finds the sum of many samples from a uniform distribution. As you'll see, the asynchronous approach -runs notably faster, since progress can be made on computing the sum of many uniform samples, while waiting for the server to -calculate and respond. +Here we'll see a simple but thorough example showing how asyncio can offer an +advantage over serial programs. +The example doesn't rely on any asyncio operators (besides the event-loop). +It's all non-blocking sockets & custom awaitables that help you see what's +actually happening under the hood and how you could do something similar. + +Performing a database request across a network might take half a second or so, +but that's ages in computer-time. +Your processor could have done millions or even billions of things. +The same is true for, say, requesting a website, downloading a car, loading a +file from disk into memory, etc. +The general theme is those are all input/output (I/O) actions. + +Consider performing two tasks: requesting some information from a server and +doing some computation locally. +A serial approach would look like: ping the server, idle while waiting for a +response, receive the response, perform the local computation. +An asynchronous approach would look like: ping the server, do some of the +local computation while waiting for a response, check if the server is ready +yet, do a bit more of the local computation, check again, etc. +Basically we're freeing up the CPU to do other activities instead of scratching +its belly button. + +This example has a server (a separate, local process) compute the sum of many +samples from a Gaussian (i.e. normal) distribution. +And the local computation finds the sum of many samples from a uniform +distribution. +As you'll see, the asynchronous approach runs notably faster, since progress +can be made on computing the sum of many uniform samples, while waiting for +the server to calculate and respond. ===================== Serial output @@ -628,11 +729,15 @@ Code Now, we'll explore some of the most important snippets. -Below is the portion of the asynchronous approach responsible for checking if the server's done yet. And, -if not, yielding control back to the event-loop instead of idly waiting. I'd like to draw your attention -to a specific part of this snippet. Setting a socket to non-blocking mode means the ``recv()`` call -won't idle while waiting for a response. Instead, if there's no data to be read, it'll immediately -raise a ``BlockingIOError``. If there is data available, the ``recv()`` will proceed as normal. +Below is the portion of the asynchronous approach responsible for checking if +the server's done yet. +And, if not, yielding control back to the event-loop instead of idly waiting. +I'd like to draw your attention to a specific part of this snippet. +Setting a socket to non-blocking mode means the ``recv()`` call won't idle while +waiting for a response. +Instead, if there's no data to be read, it'll immediately raise a +``BlockingIOError``. +If there is data available, the ``recv()`` will proceed as normal. .. code-block:: python @@ -662,7 +767,8 @@ raise a ``BlockingIOError``. If there is data available, the ``recv()`` will pro And this is the portion of code responsible for asynchronously computing -the uniform sums. It's designed to allow for working through the sum a portion at a time. +the uniform sums. +It's designed to allow for working through the sum a portion at a time. The ``time_allotment`` argument to the coroutine function decides how long the sum function will iterate, in other words, synchronously hog control, before ceding back to the event-loop. @@ -684,13 +790,16 @@ back to the event-loop. return total -The above snippet was simplified a bit. Reading ``time.time()`` and evaluating an if-condition -on every iteration for many, many iterations (in this case roughly a hundred million) more than -eats up the runtime savings associated with the asynchronous approach. The actual -implementation involves chunking the iteration, so you only perform the check -every few million iterations. With that change, the asynchronous approach wins in a landslide. -This is important to keep in mind. Too much checking or constantly jumping between tasks can -ultimately cause more harm than good! +The above snippet was simplified a bit. Reading ``time.time()`` and evaluating +an if-condition on every iteration for many, many iterations (in this case +roughly a hundred million) more than eats up the runtime savings associated +with the asynchronous approach. +The actual implementation involves chunking the iteration, so you only perform +the check every few million iterations. +With that change, the asynchronous approach wins in a landslide. +This is important to keep in mind. +Too much checking or constantly jumping between tasks can ultimately cause more +harm than good! The server, async & serial programs are available in full here: https://github.com/anordin95/a-conceptual-overview-of-asyncio/tree/main/barebones-network-io-example. @@ -705,32 +814,43 @@ Which concurrency do I want multiprocessing =========================== -For any computationally bound work in Python, you likely want to use multiprocessing. -Otherwise, the Global Interpreter Lock (GIL) will generally get in your way! For those who don't -know, the GIL is a lock which ensures only one Python instruction is executed at a time. -Of course, since processes are generally entirely independent from one another, the GIL in one -process won't be impeded by the GIL in another process. Granted, I believe there are ways to -also get around the GIL in a single process by leveraging C extensions. +For any computationally bound work in Python, you likely want to use +multiprocessing. +Otherwise, the Global Interpreter Lock (GIL) will generally get in your way! +For those who don't know, the GIL is a lock which ensures only one Python +instruction is executed at a time. +Of course, since processes are generally entirely independent from one another, +the GIL in one process won't be impeded by the GIL in another process. +Granted, I believe there are ways to also get around the GIL in a single process +by leveraging C extensions. =========================== multithreading & asyncio =========================== -Multithreading and asyncio are much more similar in where they're useful with Python: not at all for -computationally-bound work, and crucially for I/O bound work. For applications that need to -manage absolutely tons of distinct I/O connections or chunks-of-work, asyncio is a must. For example, -a web server handling thousands of requests "simultaneously" (in quotes, because, as we saw, the frequent -handoffs of control only create the illusion of simultaneous execution). Otherwise, I think the choice -between which to use is somewhat down to taste. - -Multithreading maintains an OS managed thread for each chunk of work. Whereas asyncio uses Tasks for -each work-chunk and manages them via the event-loop's queue. I believe the marginal overhead of one more -chunk of work is a fair bit lower for asyncio than threads, which matters a lot for applications that -need to manage many, many chunks of work. - -There are some other benefits associated with using asyncio. One is clearer visibility into when and where -interleaving occurs. The code chunk between two awaits is certainly synchronous. Another is simpler debugging, -since it's easier to attach and follow a trace and reason about code execution. With threading, the interleaving -is more of a black-box. One benefit of multithreading is not really having to worry about greedy threads -hogging execution, something that could happen with asyncio where a greedy coroutine never awaits and -effectively stalls the event-loop. +Multithreading and asyncio are much more similar in where they're useful with +Python: not at all for computationally-bound work, and crucially for I/O bound +work. +For applications that need to manage absolutely tons of distinct I/O connections +or chunks-of-work, asyncio is a must. +For example, a web server handling thousands of requests "simultaneously" +(in quotes, because, as we saw, the frequent handoffs of control only create +the illusion of simultaneous execution). +Otherwise, I think the choice between which to use is somewhat down to taste. + +Multithreading maintains an OS managed thread for each chunk of work. Whereas +asyncio uses Tasks for each work-chunk and manages them via the event-loop's +queue. +I believe the marginal overhead of one more chunk of work is a fair bit lower +for asyncio than threads, which matters a lot for applications that need to +manage many, many chunks of work. + +There are some other benefits associated with using asyncio. +One is clearer visibility into when and where interleaving occurs. +The code chunk between two awaits is certainly synchronous. +Another is simpler debugging, since it's easier to attach and follow a trace and +reason about code execution. +With threading, the interleaving is more of a black-box. +One benefit of multithreading is not really having to worry about greedy threads +hogging execution, something that could happen with asyncio where a greedy +coroutine never awaits and effectively stalls the event-loop. diff --git a/Doc/library/asyncio.rst b/Doc/library/asyncio.rst index a38f4199b53676..5684c44078e246 100644 --- a/Doc/library/asyncio.rst +++ b/Doc/library/asyncio.rst @@ -29,8 +29,8 @@ database connection libraries, distributed task queues, etc. asyncio is often a perfect fit for IO-bound and high-level **structured** network code. -If you're new to asyncio or confused by it and would like to better understand the fundmentals of how -it works check out: :ref:`a-conceputal-overview-of-asyncio`. +If you're new to asyncio or confused by it and would like to better understand +the fundmentals of how it works check out: :ref:`a-conceputal-overview-of-asyncio`. asyncio provides a set of **high-level** APIs to: From b745f88a19f29e0c596a0dae90a8344d689694e3 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Tue, 29 Jul 2025 14:57:46 -0700 Subject: [PATCH 04/70] Add reference to subinterpreters. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index bba4e911b0bafe..4c77450f1795fe 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -822,7 +822,7 @@ instruction is executed at a time. Of course, since processes are generally entirely independent from one another, the GIL in one process won't be impeded by the GIL in another process. Granted, I believe there are ways to also get around the GIL in a single process -by leveraging C extensions. +by leveraging C extensions or via subinterpreters. =========================== multithreading & asyncio From 0f2b8db7d463f9bfebdb9ecbb61444f3941cb08c Mon Sep 17 00:00:00 2001 From: anordin95 Date: Tue, 29 Jul 2025 15:10:07 -0700 Subject: [PATCH 05/70] - Significantly reduce article size. Remove both example sections & "Which concurrency do I want" section. --- .../a-conceptual-overview-of-asyncio.rst | 377 ------------------ 1 file changed, 377 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 4c77450f1795fe..6964dcc36965aa 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -10,9 +10,6 @@ This article seeks to help you build a sturdy mental model of how asyncio fundamentally works. Something that will help you understand the how and why behind the recommended patterns. -The final section, :ref:`which_concurrency_do_I_want`, zooms out a bit and -compares the common approaches to concurrency -- multiprocessing, -multithreading & asyncio -- and describes where each is most useful. During my own asyncio learning process, a few aspects particually drove my curiosity (read: drove me nuts). @@ -27,15 +24,6 @@ of this article. - How would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, etc. -The first two sections feature some examples but are generally focused on theory -and explaining concepts. -The next two sections are centered around examples, focused on further -illustrating and reinforcing ideas practically. - -.. contents:: Sections - :depth: 1 - :local: - --------------------------------------------- A conceptual overview part 1: the high-level --------------------------------------------- @@ -489,368 +477,3 @@ For reference, you could implement it without futures, like so:: else: await YieldToEventLoop() - -.. _anaylzing-control-flow-example: - ----------------------------------------------- -Analyzing an example programs control flow ----------------------------------------------- - -We'll walkthrough, step by step, a simple asynchronous program following along -in the key methods of Task & Future that are leveraged when asyncio is -orchestrating the show. - - -=============== -Task.step -=============== - -The actual method that invokes a Tasks coroutine: -``asyncio.tasks.Task.__step_run_and_handle_result`` is about 80 lines long. -For the sake of clarity, I've removed all of the edge-case error handling, -simplified some aspects and renamed it, but the core logic remains unchanged. - -:: - - 1 class Task(Future): - 2 ... - 3 def step(self): - 4 try: - 5 awaited_task = self.coro.send(None) - 6 except StopIteration as e: - 7 super().set_result(e.value) - 8 else: - 9 awaited_task.add_done_callback(self.__step) - 10 ... - - -====================== -Example program -====================== - -:: - - # Filename: program.py - 1 async def triple(val: int): - 2 return val * 3 - 3 - 4 async def main(): - 5 triple_task = asyncio.Task(coro=triple(val=5)) - 6 tripled_val = await triple_task - 7 return tripled_val + 2 - 8 - 9 loop = asyncio.new_event_loop() - 10 main_task = asyncio.Task(main(), loop=loop) - 11 loop.run_forever() - -===================== -Control flow -===================== - -At a high-level, this is how control flows: - -.. code-block:: none - - 1 program - 2 event-loop - 3 main_task.step - 4 main() - 5 triple_task.__await__ - 6 main() - 7 main_task.step - 8 event-loop - 9 triple_task.step - 10 triple() - 11 triple_task.step - 12 event-loop - 13 main_task.step - 14 triple_task.__await__ - 15 main() - 16 main_task.step - 17 event-loop - -And, in much more detail: - -1. Control begins in ``program.py`` - Line 9 creates an event-loop, line 10 creates ``main_task`` and adds it to - the event-loop, line 11 indefinitely passes control to the event-loop. -2. Control is now in the event-loop - The event-loop pops ``main_task`` off its queue, then invokes it by calling - ``main_task.step()``. -3. Control is now in ``main_task.step`` - We enter the try-block on line 4 then begin the coroutine ``main()`` on - line 5. -4. Control is now in the coroutine: ``main()`` - The Task ``triple_task`` is created on line 5 which adds it to the - event-loops queue. Line 6 ``await``\ s triple_task. - Remember, that calls ``Task.__await__`` then percolates any ``yield``\ s. -5. Control is now in ``triple_task.__await__`` - ``triple_task`` is not done given it was just created, so we enter - the first if-block on line 5 and ``yield`` the thing we'll be waiting - for -- ``triple_task``. -6. Control is now in the coroutine: ``main()`` - ``await`` percolates the ``yield`` and the yielded value -- ``triple_task``. -7. Control is now in ``main_task.step`` - The variable ``awaited_task`` is ``triple_task``. - No ``StopIteration`` was raised so the else in the try-block on line 8 - executes. - A done-callback: ``main_task.step`` is added to the ``triple_task``. - The step method ends and returns to the event-loop. -8. Control is now in the event-loop - The event-loop cycles to the next task in its queue. - The event-loop pops ``triple_task`` from its queue and invokes it by - calling ``triple_task.step()``. -9. Control is now in ``triple_task.step`` - We enter the try-block on line 4 then begin the coroutine ``triple()`` - via line 5. -10. Control is now in the coroutine: ``triple()`` - It computes 3 times 5, then finishes and raises a ``StopIteration`` - exception. -11. Control is now in ``triple_task.step`` - The ``StopIteration`` exception is caught so we go to line 7. - The return value of the coroutine ``triple()`` is embedded in the value - attribute of that exception. - ``Future.set_result()`` saves the result, marks the task as done and adds - the done-callbacks of ``triple_task`` to the event-loops queue. - The step method ends and returns control to the event-loop. -12. Control is now in the event-loop - The event-loop cycles to the next task in its queue. - The event-loop pops ``main_task`` and resumes it by calling - ``main_task.step()``. -13. Control is now in ``main_task.step`` - We enter the try-block on line 4 then resume the coroutine ``main`` - which will pick up again from where it ``yield``-ed. - Recall, it ``yield``-ed not in the coroutine, but in - ``triple_task.__await__`` on line 6. -14. Control is now in ``triple_task.__await__`` - We evaluate the if-statement on line 8 which ensures that ``triple_task`` - was completed. - Then, it returns the result of ``triple_task`` which was saved earlier. - Finally that result is returned to the caller - (i.e. ``... = await triple_task``). -15. Control is now in the coroutine: ``main()`` - ``tripled_val`` is 15. The coroutine finishes and raises a - ``StopIteration`` exception with the return value of 17 attached. -16. Control is now in ``main_task.step`` - The ``StopIteration`` exception is caught and ``main_task`` is marked - as done and its result is saved. - The step method ends and returns control to the event-loop. -17. Control is now in the event-loop - There's nothing in the queue. - The event-loop cycles aimlessly onwards. - ----------------------------------------------- -Barebones network I/O example ----------------------------------------------- - -Here we'll see a simple but thorough example showing how asyncio can offer an -advantage over serial programs. -The example doesn't rely on any asyncio operators (besides the event-loop). -It's all non-blocking sockets & custom awaitables that help you see what's -actually happening under the hood and how you could do something similar. - -Performing a database request across a network might take half a second or so, -but that's ages in computer-time. -Your processor could have done millions or even billions of things. -The same is true for, say, requesting a website, downloading a car, loading a -file from disk into memory, etc. -The general theme is those are all input/output (I/O) actions. - -Consider performing two tasks: requesting some information from a server and -doing some computation locally. -A serial approach would look like: ping the server, idle while waiting for a -response, receive the response, perform the local computation. -An asynchronous approach would look like: ping the server, do some of the -local computation while waiting for a response, check if the server is ready -yet, do a bit more of the local computation, check again, etc. -Basically we're freeing up the CPU to do other activities instead of scratching -its belly button. - -This example has a server (a separate, local process) compute the sum of many -samples from a Gaussian (i.e. normal) distribution. -And the local computation finds the sum of many samples from a uniform -distribution. -As you'll see, the asynchronous approach runs notably faster, since progress -can be made on computing the sum of many uniform samples, while waiting for -the server to calculate and respond. - -===================== -Serial output -===================== - -.. code-block:: none - - $ python serial_approach.py - Beginning server_request. - ====== Done server_request. total: -2869.04. Ran for: 2.77s. ====== - Beginning uniform_sum. - ====== Done uniform_sum. total: 60001676.02. Ran for: 4.77s. ====== - Total time elapsed: 7.54s. - -===================== -Asynchronous output -===================== - -.. code-block:: none - - $ python async_approach.py - Beginning uniform_sum. - Pausing uniform_sum at sample_num: 26,999,999. time_elapsed: 1.01s. - - Beginning server_request. - Pausing server_request. time_elapsed: 0.00s. - - Resuming uniform_sum. - Pausing uniform_sum at sample_num: 53,999,999. time_elapsed: 1.05s. - - Resuming server_request. - Pausing server_request. time_elapsed: 0.00s. - - Resuming uniform_sum. - Pausing uniform_sum at sample_num: 80,999,999. time_elapsed: 1.05s. - - Resuming server_request. - Pausing server_request. time_elapsed: 0.00s. - - Resuming uniform_sum. - Pausing uniform_sum at sample_num: 107,999,999. time_elapsed: 1.04s. - - Resuming server_request. - ====== Done server_request. total: -2722.46. ====== - - Resuming uniform_sum. - ====== Done uniform_sum. total: 59999087.62 ====== - - Total time elapsed: 4.60s. - -====================== -Code -====================== - -Now, we'll explore some of the most important snippets. - -Below is the portion of the asynchronous approach responsible for checking if -the server's done yet. -And, if not, yielding control back to the event-loop instead of idly waiting. -I'd like to draw your attention to a specific part of this snippet. -Setting a socket to non-blocking mode means the ``recv()`` call won't idle while -waiting for a response. -Instead, if there's no data to be read, it'll immediately raise a -``BlockingIOError``. -If there is data available, the ``recv()`` will proceed as normal. - -.. code-block:: python - - class YieldToEventLoop: - def __await__(self): - yield - ... - - async def get_server_data(): - client = socket.socket() - client.connect(server.SERVER_ADDRESS) - client.setblocking(False) - - while True: - try: - # For reference, the first argument to recv() is the maximum number - # of bytes to attempt to read. Setting it to 4096 means we could get 2 - # bytes or 4 bytes, or even 4091 bytes, but not 4097 bytes back. - # However, if there are no bytes available to be read, this recv() - # will raise a BlockingIOError since the socket was set to - # non-blocking mode. - response = client.recv(4096) - break - except BlockingIOError: - await YieldToEventLoop() - return response - - -And this is the portion of code responsible for asynchronously computing -the uniform sums. -It's designed to allow for working through the sum a portion at a time. -The ``time_allotment`` argument to the coroutine function decides how long the sum -function will iterate, in other words, synchronously hog control, before ceding -back to the event-loop. - -.. code-block:: python - - async def uniform_sum(n_samples: int, time_allotment: float) -> float: - - start_time = time.time() - - total = 0.0 - for _ in range(n_samples): - total += random.random() - - time_elapsed = time.time() - start_time - if time_elapsed > time_allotment: - await YieldToEventLoop() - start_time = time.time() - - return total - -The above snippet was simplified a bit. Reading ``time.time()`` and evaluating -an if-condition on every iteration for many, many iterations (in this case -roughly a hundred million) more than eats up the runtime savings associated -with the asynchronous approach. -The actual implementation involves chunking the iteration, so you only perform -the check every few million iterations. -With that change, the asynchronous approach wins in a landslide. -This is important to keep in mind. -Too much checking or constantly jumping between tasks can ultimately cause more -harm than good! - -The server, async & serial programs are available in full here: -https://github.com/anordin95/a-conceptual-overview-of-asyncio/tree/main/barebones-network-io-example. - -.. _which_concurrency_do_I_want: - ------------------------------- -Which concurrency do I want ------------------------------- - -=========================== -multiprocessing -=========================== - -For any computationally bound work in Python, you likely want to use -multiprocessing. -Otherwise, the Global Interpreter Lock (GIL) will generally get in your way! -For those who don't know, the GIL is a lock which ensures only one Python -instruction is executed at a time. -Of course, since processes are generally entirely independent from one another, -the GIL in one process won't be impeded by the GIL in another process. -Granted, I believe there are ways to also get around the GIL in a single process -by leveraging C extensions or via subinterpreters. - -=========================== -multithreading & asyncio -=========================== - -Multithreading and asyncio are much more similar in where they're useful with -Python: not at all for computationally-bound work, and crucially for I/O bound -work. -For applications that need to manage absolutely tons of distinct I/O connections -or chunks-of-work, asyncio is a must. -For example, a web server handling thousands of requests "simultaneously" -(in quotes, because, as we saw, the frequent handoffs of control only create -the illusion of simultaneous execution). -Otherwise, I think the choice between which to use is somewhat down to taste. - -Multithreading maintains an OS managed thread for each chunk of work. Whereas -asyncio uses Tasks for each work-chunk and manages them via the event-loop's -queue. -I believe the marginal overhead of one more chunk of work is a fair bit lower -for asyncio than threads, which matters a lot for applications that need to -manage many, many chunks of work. - -There are some other benefits associated with using asyncio. -One is clearer visibility into when and where interleaving occurs. -The code chunk between two awaits is certainly synchronous. -Another is simpler debugging, since it's easier to attach and follow a trace and -reason about code execution. -With threading, the interleaving is more of a black-box. -One benefit of multithreading is not really having to worry about greedy threads -hogging execution, something that could happen with asyncio where a greedy -coroutine never awaits and effectively stalls the event-loop. From 27d1dcd2422f0d60040a7549a220021e6df77782 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Tue, 29 Jul 2025 15:14:43 -0700 Subject: [PATCH 06/70] Align section-header lengths with section names. --- .../a-conceptual-overview-of-asyncio.rst | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 6964dcc36965aa..d2667ee65716b1 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -32,9 +32,9 @@ In part 1, we'll cover the main, high-level building blocks of asyncio: the event-loop, coroutine functions, coroutine objects, tasks & ``await``. -========================== +========== Event Loop -========================== +========== Everything in asyncio happens relative to the event-loop. It's the star of the show and there's only one. @@ -66,9 +66,9 @@ the overall event-loop approach rather useless. event_loop = asyncio.new_event_loop() event_loop.run_forever() -==================================== +=================================== Asynchronous Functions & Coroutines -==================================== +=================================== This is a regular 'ol Python function:: @@ -113,9 +113,9 @@ Notably, the coroutine can be paused & resumed at various points within the function's body. That pausing & resuming ability is what allows for asynchronous behavior! -=========== +===== Tasks -=========== +===== Roughly speaking, tasks are coroutines (not coroutine functions) tied to an event-loop. @@ -137,9 +137,9 @@ argument optional and will add it for you if it's left unspecified:: # loop argument was left unspecified. another_special_task = asyncio.Task(coro=special_fella(magic_number=12)) -=========== +===== await -=========== +===== ``await`` is a Python keyword that's commonly used in one of two different ways:: @@ -220,9 +220,9 @@ 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. -=============================================== +============================================== coroutine.send(), await, yield & StopIteration -=============================================== +============================================== asyncio leverages those 4 components to pass around control. @@ -302,9 +302,9 @@ That might sound odd to you. Frankly, it was to me too. You might be thinking: coroutines for the sake of simplicity. Ideologically, ``yield from`` and ``await`` are quite similar. -=========== +======= Futures -=========== +======= A future is an object meant to represent a computation or process's status and result. @@ -329,9 +329,9 @@ 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. -========================== +========================= await-ing Tasks & futures -========================== +========================= ``Future`` defines an important method: ``__await__``. Below is the actual implementation (well, one line was removed for simplicity's sake) found From 3852bb1bfb388db0cf48b89ab97b497fe1cfd612 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Tue, 29 Jul 2025 15:27:34 -0700 Subject: [PATCH 07/70] - Remove reference to deleted section. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index d2667ee65716b1..56d65f2d9b91b0 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -158,8 +158,6 @@ back to the event-loops queue. In practice, it's slightly more convoluted, but not by much. In part 2, we'll walk through the details that make this possible. -And in the control flow analysis example we'll walk through, in precise detail, -the various control handoffs in an example async program. **Unlike tasks, await-ing a coroutine does not cede control!** Wrapping a coroutine in a task first, then ``await``-ing that would cede control. From 8bf6d2c486e41241da7c7b4ef3ab766c0ee2a830 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Thu, 31 Jul 2025 12:35:38 -0700 Subject: [PATCH 08/70] - 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. --- .../a-conceptual-overview-of-asyncio.rst | 155 +++++++++--------- Doc/library/asyncio-future.rst | 1 + Doc/library/asyncio-task.rst | 1 + 3 files changed, 82 insertions(+), 75 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 56d65f2d9b91b0..c00fb2ac69f7eb 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -1,8 +1,8 @@ .. _a-conceputal-overview-of-asyncio: -********************************* +******************************** A Conceptual Overview of asyncio -********************************* +******************************** :Author: Alexander Nordin @@ -16,7 +16,7 @@ curiosity (read: drove me nuts). You should be able to comfortably answer all these questions by the end of this article. -- What's roughly happening behind the scenes when an object is ``await``-ed? +- What's roughly happening behind the scenes when an object is ``await``\ ed? - How does asyncio differentiate between a task which doesn't need CPU-time to make progress towards completion, for example, a network request or file 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 --------------------------------------------- In part 1, we'll cover the main, high-level building blocks of asyncio: the -event-loop, coroutine functions, coroutine objects, tasks & ``await``. +event loop, coroutine functions, coroutine objects, tasks and ``await``. ========== Event Loop ========== -Everything in asyncio happens relative to the event-loop. +Everything in asyncio happens relative to the event loop. It's the star of the show and there's only one. It's kind of like an orchestra conductor or military general. She's behind the scenes managing resources. Some power is explicitly granted to her, but a lot of her ability to get things -done comes from the respect & cooperation of her subordinates. +done comes from the respect and cooperation of her subordinates. -In more technical terms, the event-loop contains a queue of tasks to be run. +In more technical terms, the event loop contains a queue of tasks to be run. Some tasks are added directly by you, and some indirectly by asyncio. -The event-loop pops a task from the queue and invokes it (or gives it control), +The event loop pops a task from the queue and invokes it (or gives it control), similar to calling a function. That task then runs. -Once it pauses or completes, it returns control to the event-loop. -The event-loop will then move on to the next task in its queue and invoke it. +Once it pauses or completes, it returns control to the event loop. +The event loop will then move on to the next task in its queue and invoke it. This process repeats indefinitely. -Even if the queue is empty, the event-loop continues to cycle (somewhat aimlessly). +Even if the queue is empty, the event loop continues to cycle (somewhat aimlessly). Effective overall execution relies on tasks sharing well. A greedy task could hog control and leave the other tasks to starve rendering -the overall event-loop approach rather useless. +the overall event loop approach rather useless. :: import asyncio - # This creates an event-loop and indefinitely cycles through + # This creates an event loop and indefinitely cycles through # its queue of tasks. event_loop = asyncio.new_event_loop() event_loop.run_forever() -=================================== -Asynchronous Functions & Coroutines -=================================== +===================================== +Asynchronous Functions and Coroutines +===================================== This is a regular 'ol Python function:: def hello_printer(): print( "Hi, I am a lowly, simple printer, though I have all I " - "need in life -- \nfresh paper & a loving octopus-wife." + "need in life -- \nfresh paper and a loving octopus-wife." ) 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 & a loving octopus-wife. + fresh paper and a loving octopus-wife. >>> This is an asynchronous-function or coroutine-function:: @@ -93,8 +93,9 @@ This is an asynchronous-function or coroutine-function:: f"By the way, my lucky number is: {magic_number}." ) -Calling an asynchronous function creates and returns a coroutine object. It -does not execute the function:: +Calling an asynchronous function creates and returns a +:ref:`coroutine ` object. +It does not execute the function:: >>> special_fella(magic_number=3) @@ -102,38 +103,38 @@ does not execute the function:: The terms "asynchronous function" (or "coroutine function") and "coroutine object" are often conflated as coroutine. -I find that a tad confusing. +That can be confusing! In this article, coroutine will exclusively mean "coroutine object" -- the thing produced by executing a coroutine function. That 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 & resumed at various points within the +Notably, the coroutine can be paused and resumed at various points within the function's body. -That pausing & resuming ability is what allows for asynchronous behavior! +That pausing and resuming ability is what allows for asynchronous behavior! ===== Tasks ===== -Roughly speaking, tasks are coroutines (not coroutine functions) tied to an -event-loop. +Roughly speaking, :ref:`tasks ` 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 ``await``. -When tasks are created they are automatically added to the event-loop's queue +When tasks are created they are automatically added to the event loop's queue of tasks:: - # This creates a Task object and puts it on the event-loop's queue. + # This creates a Task object and puts it on the event loop's queue. special_task = asyncio.Task(coro=special_fella(magic_number=5), loop=event_loop) -It's common to see a task instantiated without explicitly specifying the event-loop +It's common to see a task instantiated without explicitly specifying the event loop it belongs to. -Since there's only one event-loop (a global singleton), asyncio made the loop -argument optional and will add it for you if it's left unspecified:: +Since there's only one event loop, asyncio made the loop argument optional and +will add it for you if it's left unspecified:: - # This creates another Task object and puts it on the event-loop's queue. - # The task is implicitly tied to the event-loop by asyncio since the + # This creates another Task object and puts it on the event loop's queue. + # The task is implicitly tied to the event loop by asyncio since the # loop argument was left unspecified. another_special_task = asyncio.Task(coro=special_fella(magic_number=12)) @@ -141,26 +142,28 @@ argument optional and will add it for you if it's left unspecified:: await ===== -``await`` is a Python keyword that's commonly used in one of two different ways:: + +:keyword:`await` is a Python keyword that's commonly used in one of two +different ways:: await task await coroutine Unfortunately, it actually does matter which type of object await is applied to. -``await``-ing a task will cede control from the current task or coroutine to -the event-loop. +``await``\ ing a task will cede control from the current task or coroutine to +the event loop. And while doing so, add a callback to the awaited task's list of callbacks indicating it should resume the current task/coroutine when it (the -``await``-ed one) finishes. +``await``\ ed one) finishes. Said another way, when that awaited task finishes, it adds the original task -back to the event-loops queue. +back to the event loops queue. In practice, it's slightly more convoluted, but not by much. In part 2, we'll walk through the details that make this possible. **Unlike tasks, await-ing a coroutine does not cede control!** -Wrapping a coroutine in a task first, then ``await``-ing that would cede control. +Wrapping a coroutine in a task first, then ``await``\ ing that would cede control. The behavior of ``await coroutine`` is effectively the same as invoking a regular, synchronous Python function. Consider this program:: @@ -171,7 +174,7 @@ Consider this program:: print("I am coro_a(). Hi!") async def coro_b(): - print("I am coro_b(). I sure hope no one hogs the event-loop...") + print("I am coro_b(). I sure hope no one hogs the event loop...") async def main(): task_b = asyncio.Task(coro_b()) @@ -183,9 +186,9 @@ Consider this program:: asyncio.run(main()) The first statement in the coroutine ``main()`` creates ``task_b`` and places -it on the event-loops queue. -Then, ``coro_a()`` is repeatedly ``await``-ed. Control never cedes to the -event-loop which is why we see the output of all three ``coro_a()`` +it on the event loops queue. +Then, ``coro_a()`` is repeatedly ``await``\ ed. 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 @@ -193,55 +196,57 @@ invocations before ``coro_b()``'s output: 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... + I am coro_b(). I sure hope no one hogs the event loop... If we change ``await coro_a()`` to ``await asyncio.Task(coro_a())``, the behavior changes. -The coroutine ``main()`` cedes control to the event-loop with that statement. -The event-loop then works through its queue, calling ``coro_b()`` and then +The coroutine ``main()`` cedes control to the event loop with that statement. +The event loop then works through its queue, calling ``coro_b()`` and then ``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_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! ----------------------------------------------- -A conceptual overview part 2: the nuts & bolts ----------------------------------------------- +------------------------------------------------ +A conceptual overview part 2: the nuts and bolts +------------------------------------------------ Part 2 goes into detail on the mechanisms 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. -============================================== -coroutine.send(), await, yield & StopIteration -============================================== +================================================ +coroutine.send(), await, yield and StopIteration +================================================ asyncio leverages those 4 components to pass around control. -``coroutine.send(arg)`` is the method used to start or resume a coroutine. + + +:meth:`coroutine.send(arg) ` 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 started, as opposed to resumed, ``arg`` must be None. -``yield``, like usual, pauses execution and returns control to the caller. +:ref:`yield `, like usual, pauses execution and returns control to the caller. In the example below, the ``yield`` is on line 3 and the caller is ``... = await rock`` on line 11. Generally, ``await`` calls the ``__await__`` method of the given object. -``await`` also does one more very special thing: it percolates (or passes along) +``await`` also does one more very special thing: it propagates (or passes along) any yields 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 (i.e. paused) on line 3 +The coroutine picks back up from where it ``yield``\ ed (that is, paused) on line 3 and executes the remaining statements in its body. -When a coroutine finishes it raises a ``StopIteration`` exception with the +When a coroutine finishes it raises a :exc:`StopIteration` exception with the return value attached to the exception. :: @@ -294,7 +299,7 @@ That might sound odd to you. Frankly, it was to me too. You might be thinking: a generator-coroutine, a different beast entirely. 2. What about a ``yield from`` within the coroutine to a function that yields - (i.e. plain generator)? + (that is, plain generator)? ``SyntaxError: yield from not allowed in a coroutine.`` I imagine Python made this a ``SyntaxError`` to mandate only one way of using 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: Futures ======= -A future is an object meant to represent a computation or process's status and -result. +A :ref:`future ` is an object meant to represent a +computation or process'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. @@ -321,15 +326,15 @@ I said in the prior section tasks store a list of callbacks and I lied a bit. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. -Futures may be also used directly i.e. not via tasks. +Futures may be also used directly that is, not via tasks. Tasks mark themselves as done when their coroutine's 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. -========================= -await-ing Tasks & futures -========================= +=========================== +await-ing Tasks and futures +=========================== ``Future`` defines an important method: ``__await__``. Below is the actual implementation (well, one line was removed for simplicity's sake) found @@ -352,18 +357,18 @@ in the control-flow example. 11 return self.result() The ``Task`` class does not override ``Future``'s ``__await__`` implementation. -``await``-ing a task or future invokes that above ``__await__`` method and +``await``\ ing a task or future invokes that above ``__await__`` method and percolates the ``yield`` on line 6 to relinquish control to its caller, which -is generally the event-loop. +is generally the event loop. ======================== 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 (i.e. asyncio.sleep). +own variant of asynchronous sleep (that is, asyncio.sleep). -This snippet puts a few tasks on the event-loops queue and then ``await``\ s a +This snippet puts a few tasks on the event loops queue and then ``await``\ s a yet unknown coroutine wrapped in a task: ``async_sleep(3)``. We want that task to finish only after 3 seconds have elapsed, but without hogging control while waiting. @@ -374,7 +379,7 @@ hogging control while waiting. print(f"I am worker. Work work.") async def main(): - # Add a few other tasks to the event-loop, so there's something + # Add a few other tasks to the event loop, so there's something # to do while asynchronously sleeping. work_tasks = [ asyncio.Task(other_work()), @@ -407,28 +412,28 @@ will monitor how much time has elapsed and accordingly call async def async_sleep(seconds: float): future = asyncio.Future() time_to_wake = time.time() + seconds - # Add the watcher-task to the event-loop. + # Add the watcher-task to the event loop. watcher_task = asyncio.Task(_sleep_watcher(future, time_to_wake)) # Block until the future is marked as done. await future We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its -``__await__`` in order to cede control to the event-loop. +``__await__`` in order to cede control to the event loop. This is effectively the same as calling ``asyncio.sleep(0)``, but I prefer the clarity this approach offers, not to mention it's somewhat cheating to use ``asyncio.sleep`` when showcasing how to implement it! -The event-loop, as usual, cycles through its queue of tasks, giving them control, +The event loop, as usual, cycles through its queue of tasks, giving them control, and receiving control back when each task pauses or finishes. The ``watcher_task``, which runs the coroutine: ``_sleep_watcher(...)`` will be -invoked once per full cycle of the event-loop's queue. +invoked once per full cycle of the event loop's queue. On each resumption, it'll check the time and if not enough has elapsed, it'll -pause once again and return control to the event-loop. +pause once again and return control 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's queue, +Given this helper task is only invoked once per cycle of the event loop's queue, 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 the library-provided asynchronous function: diff --git a/Doc/library/asyncio-future.rst b/Doc/library/asyncio-future.rst index 32771ba72e0002..4b69e569523c58 100644 --- a/Doc/library/asyncio-future.rst +++ b/Doc/library/asyncio-future.rst @@ -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 ============= diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index b19ffa8213a971..f825ae92ec7471 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -1193,6 +1193,7 @@ Introspection .. versionadded:: 3.4 +.. _asyncio-task-obj: Task Object =========== From 397df8f45f69557df4ccd113d1c2b8d856ce56eb Mon Sep 17 00:00:00 2001 From: anordin95 Date: Thu, 31 Jul 2025 12:38:04 -0700 Subject: [PATCH 09/70] - One last title alignment. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index c00fb2ac69f7eb..4d43794b3e4854 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -24,9 +24,9 @@ of this article. - How would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, etc. ---------------------------------------------- +-------------------------------------------- A conceptual overview part 1: the high-level ---------------------------------------------- +-------------------------------------------- In part 1, we'll cover the main, high-level building blocks of asyncio: the event loop, coroutine functions, coroutine objects, tasks and ``await``. From 84aedf78359898755b92d84cd2b4895910681546 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Thu, 31 Jul 2025 12:40:53 -0700 Subject: [PATCH 10/70] - Style nit. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 4d43794b3e4854..968a6549d96bb7 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -22,7 +22,7 @@ of this article. read as opposed to a task that does need cpu-time to make progress, like computing n-factorial? - How would I go about writing my own asynchronous variant of some operation? - Something like an async sleep, database request, etc. + Something like an async sleep, database request, and so on. -------------------------------------------- A conceptual overview part 1: the high-level From 3d3e12c350a1f99128758bb2fd9bd8abb687ca71 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Thu, 31 Jul 2025 12:51:55 -0700 Subject: [PATCH 11/70] - Rework a variety of I statements. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 968a6549d96bb7..bf2130d891631b 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -322,7 +322,7 @@ a coroutine does, instead it represents the status and result of that computatio kind of like a status-light (red, yellow or green) or indicator. ``Task`` subclasses ``Future`` in order to gain these various capabilities. -I said in the prior section tasks store a list of callbacks and I lied a bit. +The prior section said tasks store a list of callbacks and it lied to you a bit. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. @@ -420,8 +420,8 @@ will monitor how much time has elapsed and accordingly call We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its ``__await__`` in order to cede control to the event loop. -This is effectively the same as calling ``asyncio.sleep(0)``, but I prefer the -clarity this approach offers, not to mention it's somewhat cheating to use +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! The event loop, as usual, cycles through its queue of tasks, giving them control, @@ -468,8 +468,8 @@ Here is the full program's output: You might feel this implementation of asynchronous sleep was unnecessarily convoluted. And, well, it was. -I wanted to showcase the versatility of futures with a simple example that -could be mimicked for more complex needs. +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): From a5fdd0e3f35d91ac2d046238825312a5736dc247 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Thu, 31 Jul 2025 12:53:33 -0700 Subject: [PATCH 12/70] Lint fix. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index bf2130d891631b..537a8fabca984d 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -468,7 +468,7 @@ Here is the full program's output: 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 +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:: From b1aef5c69808be9e4864b2a96c58c0c28903665e Mon Sep 17 00:00:00 2001 From: anordin95 Date: Thu, 31 Jul 2025 18:37:56 -0700 Subject: [PATCH 13/70] - Firm up commentary on yield from in corotuines. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 537a8fabca984d..e5bac4f9bd8d19 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -296,14 +296,15 @@ The only way to yield (or effectively cede control) from a coroutine is to That might sound odd to you. Frankly, it was to me too. You might be thinking: 1. What about a ``yield`` directly within the coroutine? The coroutine becomes - a generator-coroutine, a different beast entirely. + a generator-coroutine (or async generator), a different beast entirely. 2. What about a ``yield from`` within the coroutine to a function that yields (that is, plain generator)? ``SyntaxError: yield from not allowed in a coroutine.`` - I imagine Python made this a ``SyntaxError`` to mandate only one way of using - coroutines for the sake of simplicity. - Ideologically, ``yield from`` and ``await`` are quite similar. + This was intentionally designed for the sake of simplicity -- mandating only + one way of using coroutines. Originally ``yield`` was actually barred as well, + but was re-accepted to allow for async generators. + Ideologically, ``yield from`` and ``await`` are very similar. ======= Futures From 64a12b446005fe18e0b42cf488932f55029867b2 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Thu, 31 Jul 2025 19:48:11 -0700 Subject: [PATCH 14/70] Update language comparing await and yield from. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index e5bac4f9bd8d19..f20a0e4bd4a40f 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -304,7 +304,7 @@ That might sound odd to you. Frankly, it was to me too. You might be thinking: This was intentionally designed for the sake of simplicity -- mandating only one way of using coroutines. Originally ``yield`` was actually barred as well, but was re-accepted to allow for async generators. - Ideologically, ``yield from`` and ``await`` are very similar. + Despite that, ``yield from`` and ``await`` effectively do the same thing. ======= Futures From 0fffd4bff6956ca5665dbdf6357411589acce766 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Thu, 31 Jul 2025 19:52:07 -0700 Subject: [PATCH 15/70] - Remove await-ing Tasks and futures section --- .../a-conceptual-overview-of-asyncio.rst | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index f20a0e4bd4a40f..e04e0136b7248f 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -333,34 +333,6 @@ 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. -=========================== -await-ing Tasks and futures -=========================== - -``Future`` defines an important method: ``__await__``. Below is the actual -implementation (well, one line was removed for simplicity's sake) found -in ``asyncio.futures.Future``. -It's okay if it doesn't make complete sense now, we'll go through it in detail -in the control-flow example. - -:: - - 1 class Future: - 2 ... - 3 def __await__(self): - 4 - 5 if not self.done(): - 6 yield self - 7 - 8 if not self.done(): - 9 raise RuntimeError("await wasn't used with future") - 10 - 11 return self.result() - -The ``Task`` class does not override ``Future``'s ``__await__`` implementation. -``await``\ ing a task or future invokes that above ``__await__`` method and -percolates the ``yield`` on line 6 to relinquish control to its caller, which -is generally the event loop. ======================== A homemade asyncio.sleep From bfd1212edfbea8c8c70d83fc5ae493b3c0a213b1 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 12:31:43 -0700 Subject: [PATCH 16/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index e04e0136b7248f..8d01391875eeef 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -19,7 +19,7 @@ of this article. - What's roughly happening behind the scenes when an object is ``await``\ ed? - How does asyncio differentiate between a task which doesn't need CPU-time to make progress towards completion, for example, a network request or file - read as opposed to a task that does need cpu-time to make progress, like + read as opposed to a task that does need CPU-time to make progress, like computing n-factorial? - How would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, and so on. From c94656337f8cab9045431333f19088e2e44c9013 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 12:55:50 -0700 Subject: [PATCH 17/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 8d01391875eeef..729def9cf9b340 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -54,7 +54,7 @@ This process repeats indefinitely. Even if the queue is empty, the event loop continues to cycle (somewhat aimlessly). Effective overall execution relies on tasks sharing well. -A greedy task could hog control and leave the other tasks to starve rendering +A greedy task could hog control and leave the other tasks to starve, rendering the overall event loop approach rather useless. :: From 3fc668ccf9542d1b562841d64cf07db6ed898258 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 13:13:06 -0700 Subject: [PATCH 18/70] - Address comments related to style & writing flow. --- .../a-conceptual-overview-of-asyncio.rst | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 729def9cf9b340..3cd333a1af731d 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -37,25 +37,24 @@ Event Loop ========== Everything in asyncio happens relative to the event loop. -It's the star of the show and there's only one. +It's the star of the show. It's kind of like an orchestra conductor or military general. -She's behind the scenes managing resources. -Some power is explicitly granted to her, but a lot of her ability to get things -done comes from the respect and cooperation of her subordinates. +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 subordinates. In more technical terms, the event loop contains a queue of tasks to be run. Some tasks are added directly by you, and some indirectly by asyncio. The event loop pops a task from the queue and invokes it (or gives it control), -similar to calling a function. -That task then runs. +similar to calling a function, then that task runs. Once it pauses or completes, it returns control to the event loop. The event loop will then move on to the next task in its queue and invoke it. This process repeats indefinitely. Even if the queue is empty, the event loop continues to cycle (somewhat aimlessly). -Effective overall execution relies on tasks sharing well. -A greedy task could hog control and leave the other tasks to starve, rendering -the overall event loop approach rather useless. +Effective execution relies on tasks sharing well: a greedy task could hog +control and leave the other tasks to starve, rendering the overall event loop +approach rather useless. :: @@ -85,7 +84,11 @@ Calling a regular function invokes its logic or body:: fresh paper and a loving octopus-wife. >>> -This is an asynchronous-function or coroutine-function:: +The :ref:`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 ` object. + +:: async def special_fella(magic_number: int): print( @@ -93,9 +96,7 @@ This is an asynchronous-function or coroutine-function:: f"By the way, my lucky number is: {magic_number}." ) -Calling an asynchronous function creates and returns a -:ref:`coroutine ` object. -It does not execute the function:: +Note that calling it does not execute the function:: >>> special_fella(magic_number=3) From ebfe5420e6a0f8685cf9e2f6bbb9a6dbee51b85b Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 13:23:49 -0700 Subject: [PATCH 19/70] per-thread event loop note. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 3cd333a1af731d..8610d1f8476c48 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -131,7 +131,7 @@ of tasks:: It's common to see a task instantiated without explicitly specifying the event loop it belongs to. -Since there's only one event loop, asyncio made the loop argument optional and +Since there's only one event loop (in each thread), asyncio made the loop argument optional and will add it for you if it's left unspecified:: # This creates another Task object and puts it on the event loop's queue. From 397883795e5556c0d7be8f8575ea5b5ad0e6a132 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:03:14 -0700 Subject: [PATCH 20/70] Add section describing coroutines roots in generators. --- .../a-conceptual-overview-of-asyncio.rst | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 8610d1f8476c48..6737e82a8e362d 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -115,6 +115,42 @@ 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 are built on top of generators and +generator functions. +Recall, a generator function is a function that ``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 + ... + +Like, a coroutine function, invoking a generator function does not run it. +Instead, it provides a generator object:: + + >>> get_random_number() + + >>> + +You can "invoke" or 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 ===== From 0dd99eb8b5024a32fb01c2ec78d4717a99da4e1a Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:06:08 -0700 Subject: [PATCH 21/70] Phrasing tweak. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 6737e82a8e362d..d646c881c0e85d 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -115,8 +115,8 @@ 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 are built on top of generators and -generator functions. +Coroutines and coroutine functions were built by leveraging the functionality +of generators and generator functions. Recall, a generator function is a function that ``yield``\s, like this one:: def get_random_number(): From ff804fe0efaa8bbc2e481bbe49e0aabfbdc078ad Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:23:36 -0700 Subject: [PATCH 22/70] Use asyncio.create_task instead of asyncio.Task --- .../a-conceptual-overview-of-asyncio.rst | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index d646c881c0e85d..6a511baa53e706 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -159,21 +159,17 @@ Roughly speaking, :ref:`tasks ` 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 ``await``. -When tasks are created they are automatically added to the event loop's queue -of tasks:: +The recommended way to create tasks is via :func:`asyncio.create_task`. +Creating a task automatically adds it to the event loop's queue of tasks. - # This creates a Task object and puts it on the event loop's queue. - special_task = asyncio.Task(coro=special_fella(magic_number=5), loop=event_loop) +Since there's only one event loop (in each thread), ``asyncio`` takes care of +associating the task with the event loop for you. That is, there's no need +to specify the event loop. -It's common to see a task instantiated without explicitly specifying the event loop -it belongs to. -Since there's only one event loop (in each thread), asyncio made the loop argument optional and -will add it for you if it's left unspecified:: +:: - # This creates another Task object and puts it on the event loop's queue. - # The task is implicitly tied to the event loop by asyncio since the - # loop argument was left unspecified. - another_special_task = asyncio.Task(coro=special_fella(magic_number=12)) + # This creates a Task object and puts it on the event loop's queue. + special_task = asyncio.create_task(coro=special_fella(magic_number=5)) ===== await @@ -214,7 +210,7 @@ Consider this program:: print("I am coro_b(). I sure hope no one hogs the event loop...") async def main(): - task_b = asyncio.Task(coro_b()) + task_b = asyncio.create_task(coro_b()) num_repeats = 3 for _ in range(num_repeats): await coro_a() @@ -235,7 +231,7 @@ invocations before ``coro_b()``'s output: 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.Task(coro_a())``, the +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 works through its queue, calling ``coro_b()`` and then @@ -392,15 +388,15 @@ hogging control while waiting. # Add a few other tasks to the event loop, so there's something # to do while asynchronously sleeping. work_tasks = [ - asyncio.Task(other_work()), - asyncio.Task(other_work()), - asyncio.Task(other_work()) + 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.Task(async_sleep(3)) + await asyncio.create_task(async_sleep(3)) print( "Done asynchronous sleep at time: " f"{datetime.datetime.now().strftime("%H:%M:%S")}." @@ -423,7 +419,7 @@ will monitor how much time has elapsed and accordingly call future = asyncio.Future() time_to_wake = time.time() + seconds # Add the watcher-task to the event loop. - watcher_task = asyncio.Task(_sleep_watcher(future, time_to_wake)) + watcher_task = asyncio.create_task(_sleep_watcher(future, time_to_wake)) # Block until the future is marked as done. await future From 00a1a685c11033d46a659f12ba079c2ba2141627 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 16:25:13 -0700 Subject: [PATCH 23/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 6a511baa53e706..b84de391794590 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -189,7 +189,7 @@ the event loop. And while doing so, add a callback to the awaited task's list of callbacks indicating it should resume the current task/coroutine when it (the ``await``\ ed one) finishes. -Said another way, when that awaited task finishes, it adds the original task +In other words, when that awaited task finishes, it adds the original task back to the event loops queue. In practice, it's slightly more convoluted, but not by much. From e982f9c7279ebeb6680832c5d21dac3168dedfd0 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 16:25:34 -0700 Subject: [PATCH 24/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index b84de391794590..18f110ae3e2313 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -438,7 +438,7 @@ On each resumption, it'll check the time and if not enough has elapsed, it'll pause once again and return control 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. +infinite ``while`` loop. Given this helper task is only invoked once per cycle of the event loop's queue, you'd be correct to note that this asynchronous sleep will sleep **at least** three seconds, rather than exactly three seconds. From a033876021d4db297f5919909f03f7d06230cf4d Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:30:38 -0700 Subject: [PATCH 25/70] small phrasing. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 18f110ae3e2313..3f67bb137d8d99 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -195,7 +195,7 @@ back to the event loops queue. In practice, it's slightly more convoluted, but not by much. In part 2, we'll walk through the details that make this possible. -**Unlike tasks, await-ing a coroutine does not cede control!** +**Unlike tasks, awaiting a coroutine does not cede control!** Wrapping a coroutine in a task first, then ``await``\ ing that would cede control. The behavior of ``await coroutine`` is effectively the same as invoking a regular, synchronous Python function. From dbbc0ab36eef8a586b71323c1ab389af230966b7 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 16:31:01 -0700 Subject: [PATCH 26/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 3f67bb137d8d99..585e4ff90dac71 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -219,7 +219,7 @@ Consider this program:: asyncio.run(main()) The first statement in the coroutine ``main()`` creates ``task_b`` and places -it on the event loops queue. +it on the event loop's queue. Then, ``coro_a()`` is repeatedly ``await``\ ed. 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: From af9ba253c590ac7fb7711456cfd964961cfaf310 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:32:58 -0700 Subject: [PATCH 27/70] phrasing nit. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 585e4ff90dac71..98b9600725be4e 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -258,9 +258,7 @@ and how to make your own asynchronous operators. coroutine.send(), await, yield and StopIteration ================================================ -asyncio leverages those 4 components to pass around control. - - +asyncio leverages 4 components to pass around control. :meth:`coroutine.send(arg) ` is the method used to start or resume a coroutine. If the coroutine was paused and is now being resumed, the argument ``arg`` From 34f33354de75d152dfbf9db5a973e06138bd0f8e Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:34:39 -0700 Subject: [PATCH 28/70] style nits --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 98b9600725be4e..75c7e31d25acc0 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -175,7 +175,6 @@ to specify the event loop. await ===== - :keyword:`await` is a Python keyword that's commonly used in one of two different ways:: @@ -275,7 +274,7 @@ any yields 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 (that is, paused) on line 3 +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 to the exception. From dca3d382d2f897cd29a780ad35e843ad99e59eb0 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 16:35:51 -0700 Subject: [PATCH 29/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 75c7e31d25acc0..bc131576824ead 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -276,7 +276,7 @@ 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 +When a coroutine finishes, it raises a :exc:`StopIteration` exception with the return value attached to the exception. :: From db4ac35b1036cd4d6db063a9da317f005abc4102 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:37:06 -0700 Subject: [PATCH 30/70] phrasing nit --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index bc131576824ead..d04c415cf6def7 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -277,7 +277,7 @@ 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 to the exception. +return value attached as an attribute. :: From bb8d0189bae137dbd10063fd29a0b4f0478428e6 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:42:45 -0700 Subject: [PATCH 31/70] Fix misnaming of async generator. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index d04c415cf6def7..371f8440e98f30 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -326,10 +326,10 @@ The only way to yield (or effectively cede control) from a coroutine is to That might sound odd to you. Frankly, it was to me too. You might be thinking: 1. What about a ``yield`` directly within the coroutine? The coroutine becomes - a generator-coroutine (or async generator), a different beast entirely. + an async generator, a different beast entirely. 2. What about a ``yield from`` within the coroutine to a function that yields - (that is, plain generator)? + (that is, a plain generator)? ``SyntaxError: yield from not allowed in a coroutine.`` This was intentionally designed for the sake of simplicity -- mandating only one way of using coroutines. Originally ``yield`` was actually barred as well, From 5fdd4e97d667494ce3f66fab382c097992ebbc2c Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:55:10 -0700 Subject: [PATCH 32/70] phrasing nits. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 371f8440e98f30..914c0155ab45f2 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -162,7 +162,7 @@ clear in a moment when we discuss ``await``. The recommended way to create tasks is via :func:`asyncio.create_task`. Creating a task automatically adds it to the event loop's queue of tasks. -Since there's only one event loop (in each thread), ``asyncio`` takes care of +Since there's only one event loop (in each thread), ``asyncio`` takes care of associating the task with the event loop for you. That is, there's no need to specify the event loop. @@ -341,18 +341,18 @@ Futures ======= A :ref:`future ` is an object meant to represent a -computation or process's status and result. +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. +"pending", "cancelled" or "done". Another is its result which is set when the state transitions to done. -To be clear, a future does not represent the actual computation to be done, like -a coroutine does, instead it represents the status and result of that computation, -kind of like a status-light (red, yellow or green) or indicator. +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. -``Task`` subclasses ``Future`` in order to gain these various capabilities. +:class:`asyncio.Task` subclasses :class:`asyncio.Future` in order to gain these various capabilities. The prior section said tasks store a list of callbacks and it lied to you a bit. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. From fe3c732e8d80bea22fd89f2083903f686e72756b Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 16:55:45 -0700 Subject: [PATCH 33/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 914c0155ab45f2..2f5bc78dc11eae 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -353,7 +353,7 @@ 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 and it lied to you a bit. +The prior section said tasks store a list of callbacks, which wasn't entirely true. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. From 1abe9a11d9a03a22085020e9ca639b4a4af5072f Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 16:56:13 -0700 Subject: [PATCH 34/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 2f5bc78dc11eae..96caad088a7757 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -358,7 +358,7 @@ It's actually the ``Future`` class that implements this logic which ``Task`` inherits. Futures may be also used directly that is, not via tasks. -Tasks mark themselves as done when their coroutine's complete. +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. From eadc0fb01cd7a967aa33a2f737cfc79e1b9000c0 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 16:58:22 -0700 Subject: [PATCH 35/70] consistent spacing --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 96caad088a7757..c9caeaca1f7179 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -31,7 +31,6 @@ A conceptual overview part 1: the high-level In part 1, we'll cover the main, high-level building blocks of asyncio: the event loop, coroutine functions, coroutine objects, tasks and ``await``. - ========== Event Loop ========== @@ -149,8 +148,6 @@ For example:: Hello 7 - - ===== Tasks ===== @@ -243,7 +240,6 @@ The event loop then works through its queue, calling ``coro_b()`` and then I am coro_a(). Hi! I am coro_a(). Hi! - ------------------------------------------------ A conceptual overview part 2: the nuts and bolts ------------------------------------------------ @@ -363,7 +359,6 @@ 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 ======================== @@ -420,7 +415,6 @@ will monitor how much time has elapsed and accordingly call # Block until the future is marked as done. await future - We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its ``__await__`` in order to cede control to the event loop. This is effectively the same as calling ``asyncio.sleep(0)``, but this approach From 1f7323d1e16d9f6014e69f727d179345ff279b00 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 16:59:09 -0700 Subject: [PATCH 36/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index c9caeaca1f7179..471eb53ba69465 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -364,7 +364,7 @@ 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 (that is, asyncio.sleep). +own variant of asynchronous sleep (:func:`asyncio.sleep`). This snippet puts a few tasks on the event loops queue and then ``await``\ s a yet unknown coroutine wrapped in a task: ``async_sleep(3)``. From 99ac489cae095d783ade52c6add9c3a686d3c169 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 17:08:51 -0700 Subject: [PATCH 37/70] phrasing nits --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 471eb53ba69465..8f77a15caf2701 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -364,12 +364,12 @@ 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 (:func:`asyncio.sleep`). +own variant of asynchronous sleep -- :func:`asyncio.sleep`. -This snippet puts a few tasks on the event loops queue and then ``await``\ s a -yet unknown coroutine wrapped in a task: ``async_sleep(3)``. +This snippet puts a few tasks on the event loop's queue and then ``await``\ s a +coroutine wrapped in a task: ``async_sleep(3)``. We want that task to finish only after 3 seconds have elapsed, but without -hogging control while waiting. +preventing other tasks from running. :: From feb863470df6741c9af6f13e8acd57d941b288b9 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 17:09:12 -0700 Subject: [PATCH 38/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 8f77a15caf2701..b1d0fbadc16dc3 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -399,8 +399,8 @@ preventing other tasks from running. Below, we use a future to enable custom control over when that task will be marked as done. -If ``future.set_result()``, the method responsible for marking that future as -done, is never called, this task will never finish. +If ``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()``. From 49e9c6576665c4da1cf3bd9b0afa1335891a650a Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 17:09:48 -0700 Subject: [PATCH 39/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index b1d0fbadc16dc3..11f58cb4f8883e 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -418,7 +418,7 @@ will monitor how much time has elapsed and accordingly call We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its ``__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 +offers more clarity, not to mention it's somewhat cheating to use ``asyncio.sleep`` when showcasing how to implement it! The event loop, as usual, cycles through its queue of tasks, giving them control, From daba1310eff83b6f63c093be24ec64926e0a1602 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 17:10:12 -0700 Subject: [PATCH 40/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 11f58cb4f8883e..ef3b1e16cb87cb 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -421,8 +421,8 @@ 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! -The event loop, as usual, cycles through its queue of tasks, giving them control, -and receiving control back when each task pauses or finishes. +As usual, the event loop cycles through its queue of 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's queue. On each resumption, it'll check the time and if not enough has elapsed, it'll From c31f3c5678adb5c3b1c3d815b43ade15755f6d5f Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Fri, 1 Aug 2025 17:11:48 -0700 Subject: [PATCH 41/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index ef3b1e16cb87cb..c366bc02554eac 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -425,7 +425,7 @@ As usual, the event loop cycles through its queue of 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's queue. -On each resumption, it'll check the time and if not enough has elapsed, it'll +On each resumption, it'll check the time and if not enough has elapsed, then it'll pause once again and return control 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 From 6756257dcdccc114cab01364ee5d8fa17f3630e1 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Fri, 1 Aug 2025 17:23:11 -0700 Subject: [PATCH 42/70] add conclusion --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index c366bc02554eac..ad33be9004def2 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -368,7 +368,7 @@ own variant of asynchronous sleep -- :func:`asyncio.sleep`. This snippet puts a few tasks on the event loop's queue and then ``await``\ s a coroutine wrapped in a task: ``async_sleep(3)``. -We want that task to finish only after 3 seconds have elapsed, but without +We want that task to finish only after three seconds have elapsed, but without preventing other tasks from running. :: @@ -431,10 +431,9 @@ 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's queue, -you'd be correct to note that this asynchronous sleep will sleep **at least** +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 the library-provided asynchronous function: -``asyncio.sleep``. +Note, this is also of true of: ``asyncio.sleep``. :: @@ -477,3 +476,5 @@ For reference, you could implement it without futures, like so:: else: await YieldToEventLoop() +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:`docs `. From a730bd30482676e38fdb8d50579dce554226eeb6 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 13:15:42 -0700 Subject: [PATCH 43/70] nits --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index ad33be9004def2..38c183724d529d 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -104,8 +104,10 @@ Note that calling it does not execute the function:: The terms "asynchronous function" (or "coroutine function") and "coroutine object" are often conflated as coroutine. That can be confusing! -In this article, coroutine will exclusively mean "coroutine object" -- the -thing produced by executing a coroutine function. +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. That coroutine represents the function's body or logic. A coroutine has to be explicitly started; again, merely creating the coroutine @@ -426,7 +428,7 @@ 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's queue. On each resumption, it'll check the time and if not enough has elapsed, then it'll -pause once again and return control to the event loop. +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. From 8fca2e386bdd1197b512f022279575e90fe9def8 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 14:18:17 -0700 Subject: [PATCH 44/70] - Variety of style & grammar improvements thanks to ZeroIntensity's comments. --- .../a-conceptual-overview-of-asyncio.rst | 98 ++++++++++--------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 38c183724d529d..e5e9310ae279ab 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -6,21 +6,19 @@ A Conceptual Overview of asyncio :Author: Alexander Nordin -This article seeks to help you build a sturdy mental model of how asyncio -fundamentally works. -Something that will help you understand the how and why behind the recommended -patterns. +This article seeks to help you build a sturdy mental model of how ``asyncio`` +fundamentally works, helping you understand the how and why behind the +recommended patterns. -During my own asyncio learning process, a few aspects particually drove my +During my own ``asyncio`` learning process, a few aspects particually drove my curiosity (read: drove me nuts). You should be able to comfortably answer all these questions by the end of this article. -- What's roughly happening behind the scenes when an object is ``await``\ ed? -- How does asyncio differentiate between a task which doesn't need CPU-time - to make progress towards completion, for example, a network request or file - read as opposed to a task that does need CPU-time to make progress, like - computing n-factorial? +- What's happening behind the scenes when an object is ``await``\ ed? +- How does ``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 would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, and so on. @@ -28,28 +26,30 @@ of this article. A conceptual overview part 1: the high-level -------------------------------------------- -In part 1, we'll cover the main, high-level building blocks of asyncio: the -event loop, coroutine functions, coroutine objects, tasks and ``await``. +In part 1, we'll cover the main, high-level building blocks of ``asyncio``: +the event loop, coroutine functions, coroutine objects, tasks and ``await``. ========== Event Loop ========== -Everything in asyncio happens relative to the event loop. +Everything in ``asyncio`` happens relative to the event loop. It's the star of the show. It's kind of like an orchestra conductor or military general. 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 subordinates. -In more technical terms, the event loop contains a queue of tasks to be run. -Some tasks are added directly by you, and some indirectly by asyncio. -The event loop pops a task from the queue and invokes it (or gives it control), +In more technical terms, the event loop contains a queue of tasks (or "chunks +of work") to be run. +Some tasks are added directly by you, and some indirectly by ``asyncio``. +The event loop pops a task from the queue and invokes it (or "gives it control"), similar to calling a function, then that task runs. Once it pauses or completes, it returns control to the event loop. The event loop will then move on to the next task in its queue and invoke it. This process repeats indefinitely. -Even if the queue is empty, the event loop continues to cycle (somewhat aimlessly). +Even if the queue is empty, the event loop continues to cycle (somewhat +aimlessly). Effective execution relies on tasks sharing well: a greedy task could hog control and leave the other tasks to starve, rendering the overall event loop @@ -84,7 +84,7 @@ Calling a regular function invokes its logic or body:: >>> The :ref:`async def `, as opposed to just a plain ``def``, makes -this an asynchronous function (or coroutine function). +this an asynchronous function (or "coroutine function"). Calling it creates and returns a :ref:`coroutine ` object. :: @@ -101,15 +101,15 @@ Note that calling it does not execute the function:: >>> -The terms "asynchronous function" (or "coroutine function") and "coroutine object" -are often conflated as coroutine. +The terms "asynchronous 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. -That coroutine represents the function's body or logic. +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 @@ -117,8 +117,9 @@ 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 generators and generator functions. -Recall, a generator function is a function that ``yield``\s, like this one:: +of :term:`generators ` and +:term:`generator functions `. +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! @@ -130,7 +131,7 @@ Recall, a generator function is a function that ``yield``\s, like this one:: yield 4 ... -Like, a coroutine function, invoking a generator function does not run it. +Similar to a coroutine function, calling a generator function does not run it. Instead, it provides a generator object:: >>> get_random_number() @@ -162,7 +163,7 @@ The recommended way to create tasks is via :func:`asyncio.create_task`. Creating a task automatically adds it to the event loop's queue of tasks. Since there's only one event loop (in each thread), ``asyncio`` takes care of -associating the task with the event loop for you. That is, there's no need +associating the task with the event loop for you. As such, there's no need to specify the event loop. :: @@ -184,7 +185,7 @@ Unfortunately, it actually does matter which type of object await is applied to. ``await``\ ing a task will cede control from the current task or coroutine to the event loop. -And while doing so, add a callback to the awaited task's list of callbacks +And while doing so, adds a callback to the awaited task's list of callbacks indicating it should resume the current task/coroutine when it (the ``await``\ ed one) finishes. In other words, when that awaited task finishes, it adds the original task @@ -246,7 +247,7 @@ The event loop then works through its queue, calling ``coro_b()`` and then A conceptual overview part 2: the nuts and bolts ------------------------------------------------ -Part 2 goes into detail on the mechanisms asyncio uses to manage control flow. +Part 2 goes into detail on the mechanisms ``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. @@ -255,27 +256,29 @@ and how to make your own asynchronous operators. coroutine.send(), await, yield and StopIteration ================================================ -asyncio leverages 4 components to pass around control. +``asyncio`` leverages 4 components to pass around control. :meth:`coroutine.send(arg) ` 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 started, as opposed to resumed, ``arg`` must be None. +If the coroutine is being started, as opposed to resumed, ``arg`` must be +``None``. :ref:`yield `, like usual, pauses execution and returns control to the caller. -In the example below, the ``yield`` is on line 3 and the caller is +In the example below, the ``yield``, on line 3, is called by ``... = await rock`` on line 11. -Generally, ``await`` calls the ``__await__`` method of the given object. -``await`` also does one more very special thing: it propagates (or passes along) -any yields it receives up the call-chain. +Generally, ``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 as an attribute. +return value attached in the :attr:`~StopIteration.value` attribute. :: @@ -317,7 +320,7 @@ That snippet produces this output: Coroutine main() finished and provided value: 23. It's worth pausing for a moment here and making sure you followed the various -ways control flow and values were passed. +ways that control flow and values were passed. The only way to yield (or effectively cede control) from a coroutine is to ``await`` an object that ``yield``\ s in its ``__await__`` method. @@ -347,15 +350,17 @@ 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 +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 true. +: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 +true. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. -Futures may be also used directly that is, not via tasks. +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 @@ -401,7 +406,7 @@ preventing other tasks from running. Below, we use a future to enable custom control over when that task will be marked as done. -If ``future.set_result()`` (the method responsible for marking that future as +If :meth:`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 @@ -417,7 +422,7 @@ will monitor how much time has elapsed and accordingly call # Block until the future is marked as done. await future -We'll use a rather bare object ``YieldToEventLoop()`` to ``yield`` from its +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 @@ -425,8 +430,8 @@ offers more clarity, not to mention it's somewhat cheating to use As usual, the event loop cycles through its queue of 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's queue. +The ``watcher_task``, which runs the coroutine ``_sleep_watcher(...)``, will +be invoked once per full cycle of the event loop's queue. 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 @@ -435,7 +440,7 @@ infinite ``while`` loop. Given this helper task is only invoked once per cycle of the event loop's queue, 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``. +Note this is also of true of ``asyncio.sleep``. :: @@ -478,5 +483,6 @@ For reference, you could implement it without futures, like so:: else: await YieldToEventLoop() -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:`docs `. +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:`docs `. From 776daebe06943f42e6ae0e0d1a9e3994fa111224 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 15:13:38 -0700 Subject: [PATCH 45/70] - Make all directives start with a 3 space indent. Then 4 thereafter. --- .../a-conceptual-overview-of-asyncio.rst | 317 +++++++++--------- 1 file changed, 157 insertions(+), 160 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index e5e9310ae279ab..779652a54246a5 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -57,12 +57,12 @@ approach rather useless. :: - import asyncio - - # This creates an event loop and indefinitely cycles through - # its queue of tasks. - event_loop = asyncio.new_event_loop() - event_loop.run_forever() + import asyncio + + # This creates an event loop and indefinitely cycles through + # its queue of tasks. + event_loop = asyncio.new_event_loop() + event_loop.run_forever() ===================================== Asynchronous Functions and Coroutines @@ -70,18 +70,17 @@ Asynchronous Functions and Coroutines This is a regular 'ol Python function:: - def hello_printer(): - print( - "Hi, I am a lowly, simple printer, though I have all I " - "need in life -- \nfresh paper and a loving octopus-wife." - ) + def hello_printer(): + print( + "Hi, I am a lowly, simple printer, though I have all I " + "need in life -- \nfresh paper and a loving octopus-wife." + ) 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 a loving octopus-wife. - >>> + >>> hello_printer() + Hi, I am a lowly, simple printer, though I have all I need in life -- + fresh paper and a loving octopus-wife. The :ref:`async def `, as opposed to just a plain ``def``, makes this an asynchronous function (or "coroutine function"). @@ -89,17 +88,16 @@ Calling it creates and returns a :ref:`coroutine ` object. :: - async def special_fella(magic_number: int): - print( - "I am a super special function. Far cooler than that printer. " - f"By the way, my lucky number is: {magic_number}." - ) + async def special_fella(magic_number: int): + print( + "I am a super special function. Far cooler than that printer. " + f"By the way, my lucky number is: {magic_number}." + ) Note that calling it does not execute the function:: - >>> special_fella(magic_number=3) - - >>> + >>> special_fella(magic_number=3) + The terms "asynchronous function" and "coroutine object" are often conflated as coroutine. @@ -121,35 +119,34 @@ of :term:`generators ` and :term:`generator functions `. 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 - ... + 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 provides a generator object:: - >>> get_random_number() - - >>> + >>> get_random_number() + You can "invoke" or 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 + >>> generator = get_random_number() + >>> next(generator) + Hi + 1 + >>> next(generator) + Hello + 7 ===== Tasks @@ -168,8 +165,8 @@ to specify the event loop. :: - # This creates a Task object and puts it on the event loop's queue. - special_task = asyncio.create_task(coro=special_fella(magic_number=5)) + # This creates a Task object and puts it on the event loop's queue. + special_task = asyncio.create_task(coro=special_fella(magic_number=5)) ===== await @@ -178,8 +175,8 @@ await :keyword:`await` is a Python keyword that's commonly used in one of two different ways:: - await task - await coroutine + await task + await coroutine Unfortunately, it actually does matter which type of object await is applied to. @@ -200,22 +197,22 @@ The behavior of ``await coroutine`` is effectively the same as invoking a regula synchronous Python function. Consider this program:: - import asyncio + import asyncio - async def coro_a(): - print("I am coro_a(). Hi!") + 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 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 + 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()) + asyncio.run(main()) The first statement in the coroutine ``main()`` creates ``task_b`` and places it on the event loop's queue. @@ -225,10 +222,10 @@ 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... + 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. @@ -238,10 +235,10 @@ The event loop then works through its queue, calling ``coro_b()`` and then .. 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! + 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! ------------------------------------------------ A conceptual overview part 2: the nuts and bolts @@ -282,42 +279,42 @@ return value attached in the :attr:`~StopIteration.value` attribute. :: - 1 class Rock: - 2 def __await__(self): - 3 value_sent_in = yield 7 - 4 print(f"Rock.__await__ resuming with value: {value_sent_in}.") - 5 return value_sent_in - 6 - 7 async def main(): - 8 print("Beginning coroutine main().") - 9 rock = Rock() - 10 print("Awaiting rock...") - 11 value_from_rock = await rock - 12 print(f"Coroutine received value: {value_from_rock} from rock.") - 13 return 23 - 14 - 15 coroutine = main() - 16 intermediate_result = coroutine.send(None) - 17 print(f"Coroutine paused and returned intermediate value: {intermediate_result}.") - 18 - 19 print(f"Resuming coroutine and sending in value: 42.") - 20 try: - 21 coroutine.send(42) - 22 except StopIteration as e: - 23 returned_value = e.value - 24 print(f"Coroutine main() finished and provided value: {returned_value}.") + 1 class Rock: + 2 def __await__(self): + 3 value_sent_in = yield 7 + 4 print(f"Rock.__await__ resuming with value: {value_sent_in}.") + 5 return value_sent_in + 6 + 7 async def main(): + 8 print("Beginning coroutine main().") + 9 rock = Rock() + 10 print("Awaiting rock...") + 11 value_from_rock = await rock + 12 print(f"Coroutine received value: {value_from_rock} from rock.") + 13 return 23 + 14 + 15 coroutine = main() + 16 intermediate_result = coroutine.send(None) + 17 print(f"Coroutine paused and returned intermediate value: {intermediate_result}.") + 18 + 19 print(f"Resuming coroutine and sending in value: 42.") + 20 try: + 21 coroutine.send(42) + 22 except StopIteration as e: + 23 returned_value = e.value + 24 print(f"Coroutine main() finished and provided value: {returned_value}.") 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. + 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. @@ -326,16 +323,16 @@ 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. Frankly, it was to me too. You might be thinking: - 1. What about a ``yield`` directly within the coroutine? The coroutine becomes - an async generator, a different beast entirely. + 1. What about a ``yield`` directly within the coroutine? The coroutine becomes + an async generator, a different beast entirely. - 2. What about a ``yield from`` within the coroutine to a function that yields - (that is, a plain generator)? - ``SyntaxError: yield from not allowed in a coroutine.`` - This was intentionally designed for the sake of simplicity -- mandating only - one way of using coroutines. Originally ``yield`` was actually barred as well, - but was re-accepted to allow for async generators. - Despite that, ``yield from`` and ``await`` effectively do the same thing. + 2. What about a ``yield from`` within the coroutine to a function that yields + (that is, a plain generator)? + ``SyntaxError: yield from not allowed in a coroutine.`` + This was intentionally designed for the sake of simplicity -- mandating only + one way of using coroutines. Originally ``yield`` was actually barred as well, + but was re-accepted to allow for async generators. + Despite that, ``yield from`` and ``await`` effectively do the same thing. ======= Futures @@ -380,28 +377,28 @@ preventing other tasks from running. :: - async def other_work(): - print(f"I am worker. 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) + async def other_work(): + print(f"I am worker. 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 @@ -414,13 +411,13 @@ will monitor how much time has elapsed and accordingly call :: - 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 + 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 We'll use a rather bare object, ``YieldToEventLoop()``, to ``yield`` from ``__await__`` in order to cede control to the event loop. @@ -444,29 +441,29 @@ Note this is also of true of ``asyncio.sleep``. :: - class YieldToEventLoop: - def __await__(self): - yield - - async def _sleep_watcher(future: asyncio.Future, time_to_wake: float): - while True: - if time.time() >= time_to_wake: - # This marks the future as done. - future.set_result(None) - break - else: - await YieldToEventLoop() + class YieldToEventLoop: + def __await__(self): + yield + + async def _sleep_watcher(future: asyncio.Future, time_to_wake: float): + 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 am worker. Work work. - I am worker. Work work. - I am worker. Work work. - Done asynchronous sleep at time: 14:52:25. + $ python custom-async-sleep.py + Beginning asynchronous sleep at time: 14:52:22. + I am worker. Work work. + I am worker. Work work. + I am worker. Work work. + Done asynchronous sleep at time: 14:52:25. You might feel this implementation of asynchronous sleep was unnecessarily convoluted. @@ -475,13 +472,13 @@ 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() + 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 From 0b795a23d82fe58595a4a2c87a3b376f2c6fa6eb Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 15:18:19 -0700 Subject: [PATCH 46/70] - Use :linenos: instead of manually writing the line numbers. --- .../a-conceptual-overview-of-asyncio.rst | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 779652a54246a5..ed2ba37831abd1 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -58,7 +58,7 @@ approach rather useless. :: import asyncio - + # This creates an event loop and indefinitely cycles through # its queue of tasks. event_loop = asyncio.new_event_loop() @@ -277,32 +277,33 @@ 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. -:: +.. code-block:: + :linenos: - 1 class Rock: - 2 def __await__(self): - 3 value_sent_in = yield 7 - 4 print(f"Rock.__await__ resuming with value: {value_sent_in}.") - 5 return value_sent_in - 6 - 7 async def main(): - 8 print("Beginning coroutine main().") - 9 rock = Rock() - 10 print("Awaiting rock...") - 11 value_from_rock = await rock - 12 print(f"Coroutine received value: {value_from_rock} from rock.") - 13 return 23 - 14 - 15 coroutine = main() - 16 intermediate_result = coroutine.send(None) - 17 print(f"Coroutine paused and returned intermediate value: {intermediate_result}.") - 18 - 19 print(f"Resuming coroutine and sending in value: 42.") - 20 try: - 21 coroutine.send(42) - 22 except StopIteration as e: - 23 returned_value = e.value - 24 print(f"Coroutine main() finished and provided value: {returned_value}.") + 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}.") That snippet produces this output: @@ -378,8 +379,8 @@ preventing other tasks from running. :: async def other_work(): - print(f"I am worker. Work work.") - + print("I am worker. Work work.") + async def main(): # Add a few other tasks to the event loop, so there's something # to do while asynchronously sleeping. @@ -444,7 +445,7 @@ Note this is also of true of ``asyncio.sleep``. class YieldToEventLoop: def __await__(self): yield - + async def _sleep_watcher(future: asyncio.Future, time_to_wake: float): while True: if time.time() >= time_to_wake: From d10eeeca5ab3c5a3bb4639ee1ed699bca8caa710 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 15:22:27 -0700 Subject: [PATCH 47/70] - Fix label typo for article. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- Doc/library/asyncio.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index ed2ba37831abd1..27abfd9adeae60 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -1,4 +1,4 @@ -.. _a-conceputal-overview-of-asyncio: +.. _a-conceptual-overview-of-asyncio: ******************************** A Conceptual Overview of asyncio diff --git a/Doc/library/asyncio.rst b/Doc/library/asyncio.rst index 5684c44078e246..d80bba3fd8e897 100644 --- a/Doc/library/asyncio.rst +++ b/Doc/library/asyncio.rst @@ -30,7 +30,7 @@ asyncio is often a perfect fit for IO-bound and high-level **structured** network code. If you're new to asyncio or confused by it and would like to better understand -the fundmentals of how it works check out: :ref:`a-conceputal-overview-of-asyncio`. +the fundmentals of how it works check out: :ref:`a-conceptual-overview-of-asyncio`. asyncio provides a set of **high-level** APIs to: From b2e90f36c841c0e74d5691eac46fd69ad94bf77e Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 19:38:30 -0700 Subject: [PATCH 48/70] fix label link. --- Doc/howto/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/index.rst b/Doc/howto/index.rst index 9a7bb0573c98a7..694ea1db9f9d69 100644 --- a/Doc/howto/index.rst +++ b/Doc/howto/index.rst @@ -39,7 +39,7 @@ Python Library Reference. General: -* :ref:`a-conceputal-overview-of-asyncio` +* :ref:`a-conceptual-overview-of-asyncio` * :ref:`annotations-howto` * :ref:`argparse-tutorial` * :ref:`descriptorhowto` From 9e07a36a1dffbe9a8515a1d5de9803bd2f25bef9 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Sat, 2 Aug 2025 22:33:31 -0700 Subject: [PATCH 49/70] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) Co-authored-by: Carol Willing Co-authored-by: Peter Bierma --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 27abfd9adeae60..2b53ece4756ae8 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -1,8 +1,8 @@ .. _a-conceptual-overview-of-asyncio: -******************************** -A Conceptual Overview of asyncio -******************************** +*************************************** +A Conceptual Overview of :mod:`asyncio` +*************************************** :Author: Alexander Nordin @@ -35,7 +35,7 @@ Event Loop Everything in ``asyncio`` happens relative to the event loop. It's the star of the show. -It's kind of like an orchestra conductor or military general. +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 subordinates. @@ -135,7 +135,7 @@ Instead, it provides a generator object:: >>> get_random_number() -You can "invoke" or proceed to the next ``yield`` of a generator by using the +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:: @@ -178,14 +178,14 @@ different ways:: await task await coroutine -Unfortunately, it actually does matter which type of object await is applied to. +Unfortunately, it does matter which type of object is awaited. ``await``\ ing a task will cede control from the current task or coroutine to the event loop. And while doing so, adds a callback to the awaited task's list of callbacks indicating it should resume the current task/coroutine when it (the ``await``\ ed one) finishes. -In other words, when that awaited task finishes, it adds the original task +In other words, when that awaited task finishes, the original task is added back to the event loops queue. In practice, it's slightly more convoluted, but not by much. @@ -322,7 +322,7 @@ ways that control flow and values were passed. 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. Frankly, it was to me too. You might be thinking: +That might sound odd to you. You might be thinking: 1. What about a ``yield`` directly within the coroutine? The coroutine becomes an async generator, a different beast entirely. From d12b29fc946b0e0081da9499b65733201511e597 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 22:57:14 -0700 Subject: [PATCH 50/70] - introduce async-sleep name --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 2b53ece4756ae8..b91bd43cf5816a 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -369,7 +369,8 @@ 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 -- :func:`asyncio.sleep`. +own variant of asynchronous sleep (``async_sleep``) which mimics +:func:`asyncio.sleep`. This snippet puts a few tasks on the event loop's queue and then ``await``\ s a coroutine wrapped in a task: ``async_sleep(3)``. From 86039b7fc70f04ff4977a994e1ed706388f09afa Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 23:26:03 -0700 Subject: [PATCH 51/70] Phrasing --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index b91bd43cf5816a..baa00e632a16c4 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -10,10 +10,9 @@ This article seeks to help you build a sturdy mental model of how ``asyncio`` fundamentally works, helping you understand the how and why behind the recommended patterns. -During my own ``asyncio`` learning process, a few aspects particually drove my -curiosity (read: drove me nuts). -You should be able to comfortably answer all these questions by the end -of this article. +You might be curious about some key ``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 ``await``\ ed? - How does ``asyncio`` differentiate between a task which doesn't need CPU-time From e5fafc4f9c3386124821c1acadad007e7704a832 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 23:29:16 -0700 Subject: [PATCH 52/70] nit --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index baa00e632a16c4..f1a48151d6e39b 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -11,7 +11,7 @@ fundamentally works, helping you understand the how and why behind the recommended patterns. You might be curious about some key ``asyncio`` concepts. -You'll be comfortably able to answer these questions by the end of this +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 ``await``\ ed? From 1dc6e5157984adc35dc6292ec33b627b5f2221de Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 23:48:49 -0700 Subject: [PATCH 53/70] ungendered octopus --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index f1a48151d6e39b..486cea477afb06 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -72,14 +72,15 @@ This is a regular 'ol Python function:: def hello_printer(): print( "Hi, I am a lowly, simple printer, though I have all I " - "need in life -- \nfresh paper and a loving octopus-wife." + "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 a loving octopus-wife. + fresh paper and my dearly beloved octopus partner in crime. The :ref:`async def `, as opposed to just a plain ``def``, makes this an asynchronous function (or "coroutine function"). From 3c0b0a474d9847d20f874ad7d82044955e48532a Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 23:51:15 -0700 Subject: [PATCH 54/70] teammates --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 486cea477afb06..cfe5b86d2c6d4f 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -37,7 +37,7 @@ 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 subordinates. +done comes from the respect and cooperation of its teammates. In more technical terms, the event loop contains a queue of tasks (or "chunks of work") to be run. From 0f3931caa2403ee03c0cfa125ecd590a61e06a7c Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 23:52:41 -0700 Subject: [PATCH 55/70] jobs --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index cfe5b86d2c6d4f..d3fe1812874336 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -39,18 +39,18 @@ 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 teammates. -In more technical terms, the event loop contains a queue of tasks (or "chunks +In more technical terms, the event loop contains a queue of jobs (or "chunks of work") to be run. -Some tasks are added directly by you, and some indirectly by ``asyncio``. -The event loop pops a task from the queue and invokes it (or "gives it control"), -similar to calling a function, then that task runs. +Some jobs are added directly by you, and some indirectly by ``asyncio``. +The event loop pops a job from the queue and invokes it (or "gives it control"), +similar to calling a function, then that job runs. Once it pauses or completes, it returns control to the event loop. -The event loop will then move on to the next task in its queue and invoke it. +The event loop will then move on to the next job in its queue and invoke it. This process repeats indefinitely. Even if the queue is empty, the event loop continues to cycle (somewhat aimlessly). -Effective execution relies on tasks sharing well: a greedy task could hog +Effective execution relies on tasks sharing well: a greedy job could hog control and leave the other tasks to starve, rendering the overall event loop approach rather useless. From 82a196709627e90b58e839d643f236387cf5590b Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sat, 2 Aug 2025 23:59:33 -0700 Subject: [PATCH 56/70] rework fella to penguin --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index d3fe1812874336..e7d4ded0b3b0cc 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -88,16 +88,16 @@ Calling it creates and returns a :ref:`coroutine ` object. :: - async def special_fella(magic_number: int): + async def loudmouth_penguin(magic_number: int): print( - "I am a super special function. Far cooler than that printer. " + "I am a super special talking penguin. Far cooler than that printer. " f"By the way, my lucky number is: {magic_number}." ) Note that calling it does not execute the function:: - >>> special_fella(magic_number=3) - + >>> loudmouth_penguin(magic_number=3) + The terms "asynchronous function" and "coroutine object" are often conflated as coroutine. @@ -166,7 +166,7 @@ to specify the event loop. :: # This creates a Task object and puts it on the event loop's queue. - special_task = asyncio.create_task(coro=special_fella(magic_number=5)) + task = asyncio.create_task(coro=loudmouth_penguin(magic_number=5)) ===== await From 9fa9fca872665968da399484d0995757b38ef600 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sun, 3 Aug 2025 15:33:06 -0700 Subject: [PATCH 57/70] - remove byline; add seealso --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index e7d4ded0b3b0cc..0fde285577eab0 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -4,8 +4,6 @@ A Conceptual Overview of :mod:`asyncio` *************************************** -:Author: Alexander Nordin - This article seeks to help you build a sturdy mental model of how ``asyncio`` fundamentally works, helping you understand the how and why behind the recommended patterns. @@ -21,6 +19,11 @@ article. - How would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, and so on. +.. seealso:: + + The `guide `_ which inspired this HOWTO article. + -------------------------------------------- A conceptual overview part 1: the high-level -------------------------------------------- From 9ff73dc83ada6856ad0690cf40f7750785548499 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sun, 3 Aug 2025 15:41:50 -0700 Subject: [PATCH 58/70] Change ref from asyncio to use seealso block. --- Doc/library/asyncio.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Doc/library/asyncio.rst b/Doc/library/asyncio.rst index d80bba3fd8e897..444db01390d922 100644 --- a/Doc/library/asyncio.rst +++ b/Doc/library/asyncio.rst @@ -29,8 +29,10 @@ database connection libraries, distributed task queues, etc. asyncio is often a perfect fit for IO-bound and high-level **structured** network code. -If you're new to asyncio or confused by it and would like to better understand -the fundmentals of how it works check out: :ref:`a-conceptual-overview-of-asyncio`. +.. seealso:: + + :ref:`a-conceptual-overview-of-asyncio` + Explanation of the fundamentals of asyncio. asyncio provides a set of **high-level** APIs to: From be9629d7e3d43728ae2343e96b90ea5a9cd189a9 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sun, 3 Aug 2025 15:47:17 -0700 Subject: [PATCH 59/70] Remove typehints. Fix indentation in one code example. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 0fde285577eab0..732d9be93d5565 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -450,14 +450,14 @@ Note this is also of true of ``asyncio.sleep``. def __await__(self): yield - async def _sleep_watcher(future: asyncio.Future, time_to_wake: float): + 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 + # This marks the future as done. + future.set_result(None) + break else: - await YieldToEventLoop() + await YieldToEventLoop() Here is the full program's output: From f7dbaa6919392bc04fef668cfedb6c4de39befda Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sun, 3 Aug 2025 15:49:32 -0700 Subject: [PATCH 60/70] Slight rephrase for clarity. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 732d9be93d5565..c361df334d9a40 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -191,7 +191,8 @@ indicating it should resume the current task/coroutine when it (the In other words, when that awaited task finishes, the original task is added back to the event loops queue. -In practice, it's slightly more convoluted, but not by much. +This is a basic, yet reliable mental model. +In practice, it's 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 cede control!** From 689d51749b0d81c9d667b98dd2aab29db83d9276 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Sun, 3 Aug 2025 16:02:22 -0700 Subject: [PATCH 61/70] Make references point to asyncio. Wrap some long lines. --- .../a-conceptual-overview-of-asyncio.rst | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index c361df334d9a40..83d0ec0ae86a29 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -1,21 +1,21 @@ .. _a-conceptual-overview-of-asyncio: -*************************************** -A Conceptual Overview of :mod:`asyncio` -*************************************** +**************************************** +A Conceptual Overview of :mod:`!asyncio` +**************************************** -This article seeks to help you build a sturdy mental model of how ``asyncio`` +This 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 ``asyncio`` concepts. +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 ``await``\ ed? -- How does ``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 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 would I go about writing my own asynchronous variant of some operation? Something like an async sleep, database request, and so on. @@ -28,14 +28,14 @@ article. A conceptual overview part 1: the high-level -------------------------------------------- -In part 1, we'll cover the main, high-level building blocks of ``asyncio``: +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 ``asyncio`` happens relative to the 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. @@ -44,7 +44,7 @@ done comes from the respect and cooperation of its teammates. In more technical terms, the event loop contains a queue of jobs (or "chunks of work") to be run. -Some jobs are added directly by you, and some indirectly by ``asyncio``. +Some jobs are added directly by you, and some indirectly by :mod:`!asyncio`. The event loop pops a job from the queue and invokes it (or "gives it control"), similar to calling a function, then that job runs. Once it pauses or completes, it returns control to the event loop. @@ -107,8 +107,9 @@ 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. +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 @@ -120,7 +121,8 @@ That pausing and resuming ability is what allows for asynchronous behavior! Coroutines and coroutine functions were built by leveraging the functionality of :term:`generators ` and :term:`generator functions `. -Recall, a generator function is a function that :keyword:`yield`\s, like this one:: +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! @@ -162,7 +164,7 @@ clear in a moment when we discuss ``await``. The recommended way to create tasks is via :func:`asyncio.create_task`. Creating a task automatically adds it to the event loop's queue of tasks. -Since there's only one event loop (in each thread), ``asyncio`` takes care of +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. @@ -196,9 +198,10 @@ In practice, it's 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 cede control!** -Wrapping a coroutine in a task first, then ``await``\ ing that would cede control. -The behavior of ``await coroutine`` is effectively the same as invoking a regular, -synchronous Python function. +Wrapping a coroutine in a task first, then ``await``\ ing 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 @@ -248,7 +251,8 @@ The event loop then works through its queue, calling ``coro_b()`` and then A conceptual overview part 2: the nuts and bolts ------------------------------------------------ -Part 2 goes into detail on the mechanisms ``asyncio`` uses to manage control flow. +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. @@ -257,16 +261,18 @@ and how to make your own asynchronous operators. coroutine.send(), await, yield and StopIteration ================================================ -``asyncio`` leverages 4 components to pass around control. +:mod:`!asyncio` leverages 4 components to pass around control. -:meth:`coroutine.send(arg) ` is the method used to start or resume a coroutine. +:meth:`coroutine.send(arg) ` 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 started, as opposed to resumed, ``arg`` must be ``None``. -:ref:`yield `, like usual, pauses execution and returns control to the caller. +:ref:`yield `, like usual, pauses execution and returns control +to the caller. In the example below, the ``yield``, on line 3, is called by ``... = await rock`` on line 11. Generally, ``await`` calls the :meth:`~object.__await__` method of the given @@ -407,10 +413,11 @@ preventing other tasks from running. 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() ` (the method responsible for marking that future as -done) is never called, then this task will never finish. +Below, we use a future to enable custom control over when that task will be +marked as done. +If :meth:`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()``. @@ -435,8 +442,8 @@ As usual, the event loop cycles through its queue of 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's queue. -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. +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. From 9da27dd92741da33b67b45f1a5341db56313fc64 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Mon, 4 Aug 2025 12:09:01 -0700 Subject: [PATCH 62/70] - Variety of style/phrasing improvements based on PR feedback. --- .../a-conceptual-overview-of-asyncio.rst | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 83d0ec0ae86a29..5540d778c594f8 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -42,8 +42,7 @@ 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 teammates. -In more technical terms, the event loop contains a queue of jobs (or "chunks -of work") to be run. +In more technical terms, the event loop contains a queue of jobs to be run. Some jobs are added directly by you, and some indirectly by :mod:`!asyncio`. The event loop pops a job from the queue and invokes it (or "gives it control"), similar to calling a function, then that job runs. @@ -183,15 +182,16 @@ different ways:: await task await coroutine -Unfortunately, it does matter which type of object is awaited. +Unfortunately, it does matter which type of object is ``await``\ ed. ``await``\ ing a task will cede control from the current task or coroutine to the event loop. -And while doing so, adds a callback to the awaited task's list of callbacks -indicating it should resume the current task/coroutine when it (the -``await``\ ed one) finishes. -In other words, when that awaited task finishes, the original task is added -back to the event loops queue. +In the process of relinquishing control, the task that's giving up control +adds a callback to the ``await``\ ed task's list of callbacks indicating it +should resume the current task/coroutine when it (the ``await``\ ed one) +finishes. +In other words, when that ``await``\ ed task finishes, the original task is +added back to the event loops queue. This is a basic, yet reliable mental model. In practice, it's slightly more complex, but not by much. @@ -268,8 +268,8 @@ 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 started, as opposed to resumed, ``arg`` must be -``None``. +If the coroutine is being used for the first time, as opposed to being resumed, +arg must be ``None``. :ref:`yield `, like usual, pauses execution and returns control to the caller. @@ -334,11 +334,13 @@ 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? The coroutine becomes - an async generator, a different beast entirely. + 1. What about a ``yield`` directly within the coroutine function? The + coroutine function becomes an + :ref:`async generator function `, a + different beast entirely. - 2. What about a ``yield from`` within the coroutine to a function that yields - (that is, a plain generator)? + 2. What about a ``yield from`` within the coroutine function to a (plain) + generator? ``SyntaxError: yield from not allowed in a coroutine.`` This was intentionally designed for the sake of simplicity -- mandating only one way of using coroutines. Originally ``yield`` was actually barred as well, From 3c0c11cff3dfe6d46cdb4c74447b1103a63f650e Mon Sep 17 00:00:00 2001 From: anordin95 Date: Mon, 4 Aug 2025 13:08:58 -0700 Subject: [PATCH 63/70] phrasing. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 5540d778c594f8..9cfc887592883c 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -197,7 +197,8 @@ This is a basic, yet reliable mental model. In practice, it's 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 cede control!** +**Unlike tasks, awaiting a coroutine does not hand control back to the event +loop!** Wrapping a coroutine in a task first, then ``await``\ ing that would cede control. The behavior of ``await coroutine`` is effectively the same as invoking a From 671cc8088dba45cec1ac6b8d17d52417ee948ca3 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Mon, 4 Aug 2025 13:14:38 -0700 Subject: [PATCH 64/70] phrasing nit. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 9cfc887592883c..deaf7b62583286 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -69,7 +69,7 @@ approach rather useless. Asynchronous Functions and Coroutines ===================================== -This is a regular 'ol Python function:: +This is a basic, boring Python function:: def hello_printer(): print( From 2a96782dc8fb74a7168648d7dcb94c94446308ca Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Mon, 4 Aug 2025 16:39:25 -0700 Subject: [PATCH 65/70] Apply suggestions from code review Co-authored-by: Peter Bierma --- .../a-conceptual-overview-of-asyncio.rst | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index deaf7b62583286..7a3ce8239ec057 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -10,7 +10,7 @@ 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. +article: - What's happening behind the scenes when an object is ``await``\ ed? - How does :mod:`!asyncio` differentiate between a task which doesn't need @@ -22,7 +22,7 @@ article. .. seealso:: The `guide `_ which inspired this HOWTO article. + tree/main>`_ that inspired this HOWTO article. -------------------------------------------- A conceptual overview part 1: the high-level @@ -45,14 +45,14 @@ done comes from the respect and cooperation of its teammates. In more technical terms, the event loop contains a queue of jobs to be run. Some jobs are added directly by you, and some indirectly by :mod:`!asyncio`. The event loop pops a job from the queue and invokes it (or "gives it control"), -similar to calling a function, then that job runs. +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 move on to the next job in its queue and invoke it. This process repeats indefinitely. Even if the queue is empty, the event loop continues to cycle (somewhat aimlessly). -Effective execution relies on tasks sharing well: a greedy job could hog +Effective execution relies on tasks sharing well; a greedy job could hog control and leave the other tasks to starve, rendering the overall event loop approach rather useless. @@ -255,7 +255,7 @@ 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 +You'll come away from this section knowing what ``await`` does behind the scenes and how to make your own asynchronous operators. ================================================ @@ -269,8 +269,8 @@ 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``. +If the coroutine is being used for the first time (as opposed to being resumed) +``arg`` must be ``None``. :ref:`yield `, like usual, pauses execution and returns control to the caller. @@ -359,7 +359,7 @@ 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. +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. @@ -367,7 +367,7 @@ 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 -true. +correct. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. @@ -498,4 +498,4 @@ For reference, you could implement it without futures, like so:: 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:`docs `. +:mod:`rest of the documentation `. From def61577b9ef1aa108ef8da2151e8fd9de8e3473 Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Mon, 4 Aug 2025 16:49:22 -0700 Subject: [PATCH 66/70] Update Doc/howto/a-conceptual-overview-of-asyncio.rst Co-authored-by: Carol Willing --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 7a3ce8239ec057..128830130579ca 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -16,8 +16,8 @@ article: - 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 would I go about writing my own asynchronous variant of some operation? - Something like an async sleep, database request, and so on. +- How to write an asynchronous variant of an operation, such as + an async sleep or database request. .. seealso:: From a84827b745b23d2bc101fc9f214089743acbf3b9 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Mon, 4 Aug 2025 17:14:51 -0700 Subject: [PATCH 67/70] nit --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 128830130579ca..2194f603bdfb07 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -367,7 +367,7 @@ 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 -correct. +accurate. It's actually the ``Future`` class that implements this logic which ``Task`` inherits. From 01710e2c2ebba26d4b7f2d5da8370818f4fcff5c Mon Sep 17 00:00:00 2001 From: Alexander Nordin Date: Mon, 4 Aug 2025 20:22:16 -0700 Subject: [PATCH 68/70] Apply suggestions from code review Co-authored-by: Carol Willing --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 2194f603bdfb07..0edabfa9c53617 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -52,7 +52,7 @@ This process repeats indefinitely. Even if the queue is empty, the event loop continues to cycle (somewhat aimlessly). -Effective execution relies on tasks sharing well; a greedy job could hog +Effective execution relies on tasks sharing well and cooperating; a greedy job could hog control and leave the other tasks to starve, rendering the overall event loop approach rather useless. @@ -96,7 +96,8 @@ Calling it creates and returns a :ref:`coroutine ` object. f"By the way, my lucky number is: {magic_number}." ) -Note that calling it does not execute the function:: +Calling the async function, `loudmouth_penguin`, does not execute the print statement; +instead, it creates a coroutine object:: >>> loudmouth_penguin(magic_number=3) @@ -134,7 +135,7 @@ one:: ... Similar to a coroutine function, calling a generator function does not run it. -Instead, it provides a generator object:: +Instead, it creates a generator object:: >>> get_random_number() @@ -262,7 +263,7 @@ and how to make your own asynchronous operators. coroutine.send(), await, yield and StopIteration ================================================ -:mod:`!asyncio` leverages 4 components to pass around control. +:mod:`!asyncio` leverages four components to pass around control. :meth:`coroutine.send(arg) ` is the method used to start or resume a coroutine. @@ -342,9 +343,9 @@ That might sound odd to you. You might be thinking: 2. What about a ``yield from`` within the coroutine function to a (plain) generator? - ``SyntaxError: yield from not allowed in a coroutine.`` + It causes the following 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. Originally ``yield`` was actually barred as well, + 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. From b5f56f38c5f8f6de97870aed274787aefb96e134 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Mon, 4 Aug 2025 20:27:55 -0700 Subject: [PATCH 69/70] fix backticks. --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index 0edabfa9c53617..f38e37b6eeb8f1 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -96,7 +96,7 @@ Calling it creates and returns a :ref:`coroutine ` object. f"By the way, my lucky number is: {magic_number}." ) -Calling the async function, `loudmouth_penguin`, does not execute the print statement; +Calling the async function, ``loudmouth_penguin``, does not execute the print statement; instead, it creates a coroutine object:: >>> loudmouth_penguin(magic_number=3) From 33445747ed621f2e12e1052bae8b22e1b4b66891 Mon Sep 17 00:00:00 2001 From: anordin95 Date: Mon, 4 Aug 2025 20:50:52 -0700 Subject: [PATCH 70/70] nits --- Doc/howto/a-conceptual-overview-of-asyncio.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/howto/a-conceptual-overview-of-asyncio.rst b/Doc/howto/a-conceptual-overview-of-asyncio.rst index f38e37b6eeb8f1..3257ef410d7088 100644 --- a/Doc/howto/a-conceptual-overview-of-asyncio.rst +++ b/Doc/howto/a-conceptual-overview-of-asyncio.rst @@ -160,7 +160,7 @@ Tasks Roughly speaking, :ref:`tasks ` 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 ``await``. +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 adds it to the event loop's queue of tasks. @@ -277,8 +277,8 @@ If the coroutine is being used for the first time (as opposed to being resumed) to the caller. In the example below, the ``yield``, on line 3, is called by ``... = await rock`` on line 11. -Generally, ``await`` calls the :meth:`~object.__await__` method of the given -object. +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.