-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
gh-137026: Add an explainer guide for asyncio #137215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
**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:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I’m not sure why this design decision was made and find it rather confuses the meaning of await: asynchronously wait. If someone here does know, please tell me so I can update this section appropriately! I think providing such context can aid significantly with peoples understanding (myself included!).
For reference, I'd imagine either having coroutines yield in their __await__
or disallowing await coroutine
entirely (thereby effectively mandating people who want to use coroutines must use coroutine.send()
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand your question correctly, it's just that coroutines act as a wrapper over other tasks. Using await
on a coroutine will cede control once that coroutine calls await
on a task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, agreed! Coroutines will give up control once await
is used on a task or future.
However, I think that coroutines not ceding on await
is somewhat surprising behavior. One of the upsides of asyncio is the precise visibility into where control handoffs happen. This behavior muddles that insight a bit. The person reading the code can't just look for await
's, they need to know the type of the object being await
ed.
I imagine most coroutines do eventually call something which will await
a task or future, but some may not. In a similar vein as that first point, this behavior feels like a sneaky gotcha that could surprise users and lead to implementations that unintentionally hog the event loop.
Finally, I don't really see a strong upside to the current behavior in the face of those downsides. I'd prefer entirely disallowing await coroutine
and instead having authors rely on coroutine.send
. But, of course, I might be missing something!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagine most coroutines do eventually call something which will await a task or future, but some may not.
It's not that intuitive, but if no future/task is called down the line, then there's no point in making it async. A function becomes async because it needs to await
something else that is async, and that should go all the way down the line until you hit a case where someone made something async to await
a future/task. If there isn't one, then there was no point in making anything async in the first place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you can still write correct or incorrect programs either way. My concern is that a well designed tool should be hard to misuse. And I don't see an upside for the synchronous await coroutine
behavior, despite this downside.
For a concrete example, consider someone working in a broader async codebase. They want to create functionality to asynchronously wait for a db request from their in-house, custom db. They write something like this. And fail to realize they're unwittingly going to stall the whole event loop, despite the various uses of await
.
async def read_from_db():
while True:
is_db_ready = await check_is_db_ready()
if is_db_ready:
db_record = await fetch_db_record()
return db_record
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's basically syntactic-sugar for yield from (i.e. the syntax that was originally used in 3.4). Therefore it's needed to enter the coroutine (which is basically another name for a generator) and run that code. In other words, anything async
needs to use await
, but it doesn't necessarily yield.
It would be more accurate to say that await
is the only place in the code where we may yield back to the event loop, but there's no guarantee unless you are familiar with the functionality of that code. e.g. In aiohttp, some users may, on the rare occasion, get caught by code processing data slower than it arrives and therefore the code never actually needs to yield to the event loop (because we're never waiting for the socket).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. @Dreamsorcerer, I'm not sure I totally follow. Let me make sure I understand!
It sounds like synchronous execution of await coroutine
wasn't an explicit design goal. But moreso a side effect of building the coroutine functionality on top of yield from
. Or, is there some upside to that design which I'm not seeing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know about explicit design, I wasn't involved. But, I assume this is related to how it was implemented (i.e. using generators and yield from
). I don't think tasks existed originally, so that element was added later.
The upside is probably performance, I suspect asyncio would be a lot slower if we yielded to the event loop on every single coroutine (think that a single await
in your code may result in several nested await
s through the call stack before reaching a coroutine that is actually meant to yield).
We actually have micro-optimisations in aiohttp where we avoid scheduling a task if we expect it to resolve without delay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhhh!! I initially figured performance wouldn't be a huge deal, but I hadn't really thought through the extent of deeply nested tasks vs. coros. Gosh that's been bugging me for a while. Thank you :)
I set up a harness to verify. The jist of it is below. One is coroutines all the way down, the other Tasks.
async def coro5(x: int):
return x + 1
...
async def coro2(x: int):
return await coro3(x) + 1
async def coro1(x: int):
return await coro2(x) + 1
async def main():
for _ in range(10_000):
output = await coro1(7)
return output
Time elapsed for the coroutine approach: 0.00419s.
Time elapsed for the Task approach: 1.48859s.
https://github.com/anordin95/a-conceptual-overview-of-asyncio/blob/20fe2ddbb375e55cc9e835fa3239cfc1ffc4ec68/hypotheses/9-await-perf-coro.py#L4-L22
https://github.com/anordin95/a-conceptual-overview-of-asyncio/blob/20fe2ddbb375e55cc9e835fa3239cfc1ffc4ec68/hypotheses/10-await-perf-task.py#L4-L22
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will review further once the lines are wrapped, as now it will invalidate suggestions.
- Start sentences on new lines to minimize disruption of diffs.
…Which concurrency do I want" section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to commit to reviewing this if nobody else has the time. I think is generally an improvement to the documentation, but definitely needs some work still. Thanks for working on this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are still many minor infringements of the Style guide (e.g. simple English) which could be addressed in one pass.
You can also use Sphinx cross references when discussing specific keywords, functions etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly LGTM. I'm down to just typos at this point.
@anordin95 @ZeroIntensity I'm going to mark this request changes as a reminder to myself that I want to make sure that we address @kumaraditya303's commments sufficiently prior to any merge (as he and myself as well as a few others are listed as asyncio experts in the devguide). Thanks for working through many of the comments and suggestions. 💪🏼 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking as request changes pending @kumaraditya303's re-review.
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
Let's just use the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @ZeroIntensity. Good idea.
Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've done another pass over the explainer. I've tried to read this from the perspective of someone less familiar with event loops.
I'm still on the fence about using queue, and these suggestions remove reference to the concept of queue.
Overall, this explainer is a nice addition.
@@ -38,6 +39,7 @@ Python Library Reference. | |||
|
|||
General: | |||
|
|||
* :ref:`a-conceptual-overview-of-asyncio` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This item should follow the argparse-tutorial. The section is alphabetized by standard library topic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree re: alphabetization. But these are rendered without the dashes and become "A Conceptual..." and "Annotations..."
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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- What's happening behind the scenes when an object is ``await``\ ed? | |
- What's happening behind the scenes when an object is awaited? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it was this way originally. If I recall correctly @ZeroIntensity suggested the switch. I now actually have a slight preference for await
ed too, but I don't feel very strongly. Y'all should decide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My original suggestion was to remove the dash from "await-ed". No preference from me on whether await
should be in a code block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, shoot. My bad!
Turns out the only impediment was my bad memory lol. Will move the various occurrences out of code blocks 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not a fan of this since when translating most languages will have to restructure the sentence to preserve the keyword.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@StanFromIreland Do you prefer keeping the backticks? I don't feel that strongly about it. If maintaining the backticks works better for translations, I'm cool with keeping them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My apologies, my comment is not very clear. I am referring to the existing one with the backticks, since it can not be translated, and it is part of the sentence, it will not make sense in another language. The translator will either have to restructure the sentence to make it somewhat work, or juggle it around (e.g. in parenthesis) as they won't want to remove it. Your suggestion is simpler to translate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So to be clear, we are removing the backticks. Correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Apologies again, I should work on my clarity) Yes, I am in favour of removing them.
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, 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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, 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). | |
Conceptually, the event loop manages jobs to be run. | |
Some jobs are added by you, and some indirectly by :mod:`!asyncio`. | |
The event loop selects a job and invokes it (or "gives it control"), | |
similar to calling a function, and then that job runs. | |
Once the job pauses or completes, it returns control to the event loop. | |
The event loop will then move on to the next job and invoke it. | |
This process repeats indefinitely. | |
Even if no jobs are ready to run, the event loop continues to cycle (somewhat | |
aimlessly). |
# This creates an event loop and indefinitely cycles through | ||
# its queue of tasks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# This creates an event loop and indefinitely cycles through | |
# its queue of tasks. | |
# This creates an event loop and indefinitely cycles through its available work tasks. |
offers more clarity, not to mention it's somewhat cheating to use | ||
``asyncio.sleep`` when showcasing how to implement it! | ||
|
||
As usual, the event loop cycles through its queue of tasks, giving them control |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As usual, the event loop cycles through its queue of tasks, giving them control | |
As usual, the event loop cycles through its tasks, giving them control |
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
be invoked once per full cycle of the event loop's queue. | |
be invoked once per full cycle of 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, |
Co-authored-by: Carol Willing <carolcode@willingconsulting.com>
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`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to point out some minor clarifications, feel free to disregard if you don't think it's appropriate in these docs:
It would be more accurate here to say that any code can queue a job, including third-party libraries etc.
The event loop pops a job from the queue and invokes it (or "gives it control"), | ||
similar to calling a function, and then that job runs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strictly speaking, it's not quite a simple queue. Maybe this only matters to low-level asyncio developers though, and general users don't need to understand the difference.
If I remember correctly, essentially, when tasks are being added, they get added to a list of tasks to be run on the next loop cycle. Once all tasks in the current list have been iterated through once (with incomplete tasks getting re-added to the next cycle's list), only then does it move to the next loop cycle, and create a new list for a future cycle.
This is probably roughly the same as the comment below: https://github.com/python/cpython/pull/137215/files#r2250000716
This process repeats indefinitely. | ||
Even if the queue is empty, the event loop continues to cycle (somewhat | ||
aimlessly). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically we use asyncio.run()
, so the loop continues until that task completes, not indefinitely.
I also don't think the loop cycles aimlessly, the application would basically sleep. e.g. If all your tasks are waiting on I/O (so there are no tasks queued), then it uses the OS's select() call (on the selector event loop) to tell the OS to wake it up when it's received data on those sockets. When it wakes up, it will reschedule the tasks associated with those sockets and run another cycle.
event_loop = asyncio.new_event_loop() | ||
event_loop.run_forever() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest we don't use the low-level APIs here if we're trying to teach users the correct way to utilise asyncio.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the clarity that this explicit approach provides. I think it helps users gain a broader understanding. But I hear you on also showing the suggested API usage. I've added a chunk to this effect in the Tasks section:
Earlier, we manually created the event loop and set it to run forever.
In practice, it's recommended to use (and common to see) :func:asyncio.run
,
which takes care of managing the event loop and ensuring the provided
coroutine finishes before advancing.
For example, many async programs follow this setup::
import asyncio
async def main():
...
if __name__ == "__main__":
asyncio.run(main())
# The program will not reach the following print statement until the
# coroutine main() finishes.
print("coroutine main() is done!")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the clarity that this explicit approach provides. I think it helps users gain a broader understanding.
I'll leave it to you, but personally I don't really understand what it's trying to explain.
Technically, as per my comment above, the comment here is not entirely accurate. This code in it's current state, creates an event loop and starts the first cycle of the loop and then basically sleeps indefinitely as I understand it. It does not cycle indefinitely, because there is no code that could cause it to move on to a second cycle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the clarity that this explicit approach provides. I think it helps users gain a broader understanding. But I hear you on also showing the suggested API usage. I've added a chunk to this effect in the Tasks section:
For new docs, you should use asyncio.run or asyncio.Runner as the runner, -1 on using older APIs which are hard to use correctly.
A task also maintains a list of callback functions whose importance will become | ||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also like how mentioning the queue subtly reinforces the idea that execution is serial (i.e. not parallel).
I would additionally say that it's useful to understand that the order of execution is predictable. Many users are under the impression that "scheduled" means unpredictable order of execution. The tasks should start executing in the order they've been added.
``await`` also does one more very special thing: it propagates (or "passes | ||
along") any ``yield``\ s it receives up the call-chain. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's essentially syntactic sugar for yield from
(which was the actual syntax used in Python 3.4).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mentioned later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I guess to me this should just point to yield from
in the docs elsewhere, rather than re-explaining how it works. But, probably doesn't matter much.
Curiously, I don't have permissions to resolve my own comments here...
and how to make your own asynchronous operators. | ||
|
||
================================================ | ||
coroutine.send(), await, yield and StopIteration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's useful to understand how it works, but I'd probably rename the header to something like "How coroutines work under-the-hood".
I use similar examples for talks explaining how asyncio works, where I start by showing the more complex things that can be done with generators (that many developers are unaware of) and end up with a very simplistic approximation of an event loop, without using any async/await syntax. (I use the functions interactively in a talk, but the code examples are at: https://gist.github.com/Dreamsorcerer/5c0ddafae6eed073a90cf52fd7cfaede)
By using generators alone with no actual asyncio code though, I think I avoid misleading anybody in thinking that the syntax they see is the appropriate way to write asyncio (which I cover with a couple of points at the end with actual asyncio examples).
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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably worth an example of why this might be a problem and how to deal with it?
For example, if you await
on things which don't yield or are processing some CPU-intensive data, you may need to add await asyncio.sleep(0)
to the code to ensure the code does yield back to the event loop in a reasonable amount of time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally on that, it might be worth mentioning the debug features of asyncio, such as the ability to log anything that blocks the loop for more than (by default) 100ms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, yeah. Good callout! Lemme stew on this a bit.
**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:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's basically syntactic-sugar for yield from (i.e. the syntax that was originally used in 3.4). Therefore it's needed to enter the coroutine (which is basically another name for a generator) and run that code. In other words, anything async
needs to use await
, but it doesn't necessarily yield.
It would be more accurate to say that await
is the only place in the code where we may yield back to the event loop, but there's no guarantee unless you are familiar with the functionality of that code. e.g. In aiohttp, some users may, on the rare occasion, get caught by code processing data slower than it arrives and therefore the code never actually needs to yield to the event loop (because we're never waiting for the socket).
I agree, using queue as the terminology here isn't completely correct so +1 to remove it. |
:: | ||
|
||
class YieldToEventLoop: | ||
def __await__(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bare yield is used internally by asyncio to implement asyncio.sleep(0) and is special cased, I don't think this is a good example to document as it relies too much on implementation detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. I don't think this is an implementation detail of asyncio
. If we momentarily ignore asyncio
completely, this (yield
ing from an __await__
) is the only way to cede control from a Python coroutine. Could you say more about it being special cased?
Instead, I think asyncio.sleep(0)
, this example and any other async
library likely use this same fundamental pattern exposed by Python.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. I don't think this is an implementation detail of asyncio.
Why? Is this documented somewhere?
Could you say more about it being special cased?
yielding None is special cased and is used for relinquishing control for one event loop iteration. You can see this in implementation of tasks.
Instead, I think asyncio.sleep(0), this example and any other async library likely use this same fundamental pattern exposed by Python.
No, asyncio libraries are built on top of futures and tasks and not objects implementing __await__
from scratch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, asyncio libraries are built on top of futures and tasks and not objects implementing
__await__
from scratch.
I think they mean alternatives to asyncio such as trio, not asyncio libraries. The point is to understand how asyncio actually works, rather than the bare minimum needed to build something on top of it. Understanding this point helps advanced users understand when (and why) code may or may not yield to the event loop.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. | |
The recommended way to create tasks is via :func:`asyncio.create_task` | |
which creates the task and schedules its execution by the event loop. |
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 hand control back to the event |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't entirely correct, using tasks does not guarantee that the control will be given to event loop if e.g. the task completes eagerly.
Explainer guide for asyncio
gh-137026: HOWTO article for asyncio, and a reference to it from the main page of the asyncio docs.
Hi!
I've used Python's
asyncio
a couple times now, but never really felt confident in my mental model of how it fundamentally works and therefore how I can best leverage it. The official docs provide good documentation for each specific function in the package, but, in my opinion, are missing a cohesive overview of the systems design and architecture. Something that could help the user understand the why and how behind the recommended patterns. And a way to help the user make informed decisions about which tool in the asyncio toolkit they ought to grab, or to recognize when asyncio is the entirely wrong toolkit. I thought I'd take a stab at filling that gap and contributing back to a community that's given so much!📚 Documentation preview 📚: https://cpython-previews--137215.org.readthedocs.build/en/137215/howto/a-conceptual-overview-of-asyncio.html