AsyncIO Explained in 45 Seconds
45sQuick, visual intro to async IO hooks viewers with promise of demystifying a complex topic.
▶ Play ClipThis video is a comprehensive guide to Python's AsyncIO library for writing concurrent code. It covers the core concepts, terminology, and practical usage with visual animations to illustrate how the event loop works. The tutorial also demonstrates how to convert synchronous code to asynchronous, when to use async vs threads vs multiprocessing, and common pitfalls to avoid.
AsyncIO is Python's built-in library for writing concurrent code using the async/await syntax. It is single-threaded and uses cooperative multitasking.
Concurrent code allows other work to happen while waiting for I/O, unlike synchronous code which blocks. AsyncIO excels at I/O-bound tasks.
The event loop is the engine that manages and runs asynchronous functions. It keeps track of tasks and resumes them when they are ready.
There are three main awaitable objects: coroutines (async functions), tasks (wrappers scheduled on the event loop), and futures (low-level results).
Awaiting a coroutine directly schedules and runs it to completion without concurrency. To run concurrently, wrap coroutines in tasks using create_task.
Creating tasks with asyncio.create_task schedules coroutines on the event loop, allowing them to run concurrently.
Using synchronous blocking code like time.sleep inside an async function blocks the entire event loop, preventing concurrency.
Use asyncio.to_thread for blocking I/O-bound code and loop.run_in_executor with a ProcessPoolExecutor for CPU-bound code.
Use asyncio.gather with return_exceptions=True to run all tasks even if some fail. Use TaskGroup when you want all tasks to succeed or fail together.
Use a profiler like Scalene to determine which parts of your code are I/O-bound vs CPU-bound, guiding where to apply async, threads, or processes.
Converting a synchronous script to async reduced download time from 13s to 1.6s and processing from 10s to 3.25s using async libraries and multiprocessing.
Use asyncio.Semaphore to limit the number of concurrent tasks (e.g., downloads) to avoid overwhelming resources.
AsyncIO is a powerful tool for I/O-bound concurrency in Python. By understanding the event loop, tasks, and proper use of async/await, you can significantly speed up your programs. Remember to profile your code and choose the right concurrency model (async, threads, or processes) based on whether your tasks are I/O-bound or CPU-bound.
"Title accurately describes the content: a complete guide with animations."
What is AsyncIO?
Python's built-in library for writing concurrent code using async/await syntax.
01:04
What is the event loop?
The engine that runs and manages asynchronous functions, keeping track of tasks and resuming them when ready.
03:58
Name the three main types of awaitable objects in AsyncIO.
Coroutines, tasks, and futures.
05:17
What happens when you await a coroutine directly?
It schedules and runs the coroutine to completion without concurrency.
08:53
How do you run coroutines concurrently?
Wrap them in tasks using async io.create_task and then await the tasks.
12:16
Why does time.sleep block the event loop?
Because it is synchronous and does not yield control to the event loop; use async io.sleep instead.
18:52
How can you run synchronous blocking I/O code in an async program?
Use async io.to_thread to run it in a separate thread.
21:14
What is the difference between async io.gather and TaskGroup regarding error handling?
Gather with return_exceptions=True continues running other tasks if one fails; TaskGroup cancels all tasks on first failure.
27:22
How do you limit the number of concurrent tasks in AsyncIO?
Use async io.Semaphore(n) and acquire it with 'async with semaphore:' before the task.
44:12
What is a common pitfall when using AsyncIO?
Forgetting to await tasks or coroutines, leading to tasks not running or script ending prematurely.
48:01
AsyncIO Definition
Core definition of the library being taught.
01:04Event Loop Analogy
Clear explanation of the event loop as the engine of async code.
03:58Common Mistake: Awaiting Coroutines Directly
Highlights a frequent error that prevents concurrency.
08:53Blocking the Event Loop
Demonstrates the critical concept that blocking calls destroy concurrency.
18:52Gather vs TaskGroup
Practical guidance on choosing between two key APIs based on error handling needs.
27:22Real-World Speedup
Concrete example showing dramatic performance improvement from 23s to 5s.
38:52[00:00] Hey there. How's it going everybody? In
[00:01] this video, we're going to be learning
[00:03] all about Async.io in Python. Async.io
[00:06] is Python's built-in library for writing
[00:08] concurrent code. And it can seem a bit
[00:10] intimidating at first with all the
[00:12] different terminology and moving parts.
[00:14] But by the end of this tutorial, I'm
[00:16] hoping that you'll have a solid
[00:17] understanding of how it works and when
[00:19] to use it. Now, in this video, I'm going
[00:21] to be covering a lot. We're going to be
[00:23] learning how Async.io actually works
[00:25] under the hood. We'll see visually
[00:27] what's happening with some animations
[00:28] that I've put together. Uh we'll see how
[00:30] to update an existing codebase to use
[00:32] async.io. We're going to be able to
[00:35] determine when and where to use async.io
[00:37] with some simple profiling. And we'll
[00:39] discuss when to choose async versus
[00:42] threads versus multiprocessing. And the
[00:45] code in the animations that I'm going to
[00:46] use in this video are going to be
[00:48] available on my GitHub and website after
[00:51] this is out. So, if you all want to
[00:53] follow along with those yourselves, then
[00:55] I'll leave links to those in the
[00:56] description section below. Now, I'm
[00:58] going to get to some code as soon as I
[01:00] can, but there's a few basics that we
[01:02] have to get out of the way first. So,
[01:04] async.io is a Python library for writing
[01:07] concurrent code using the async await
[01:10] syntax. And I want to mention that we're
[01:13] using the latest version of Python in
[01:14] this video. And I'll be teaching the
[01:17] current way to do things. Async.io has
[01:19] evolved quite a bit over the years. Uh
[01:21] there have been different ways of
[01:23] running the event loop, scheduling and
[01:25] running task and all kinds of different
[01:27] changes. Uh but we're going to focus on
[01:29] the modern ways that you should be using
[01:31] today. Now before we dive too deep,
[01:34] let's talk about what concurrency
[01:36] actually is. So with synchronous code
[01:39] execution, uh which is what we normally
[01:41] write, one thing happens after another.
[01:44] So, it's kind of like going to a Subway
[01:47] restaurant where uh you put in your
[01:49] order and they make your entire sandwich
[01:51] from start to finish uh before moving on
[01:54] to the next customer. But with
[01:56] concurrent code, it's more like going to
[01:58] a McDonald's where someone just takes
[02:01] your order and then moves on to the next
[02:04] customer while your food is being made
[02:06] in the background. And that difference
[02:08] when applied to code can be really
[02:11] confusing at first for a lot of people.
[02:13] So here's something that I think is
[02:15] important to understand pretty early on.
[02:17] So asynchronous doesn't automatically
[02:20] mean faster. It just means that we can
[02:22] do other useful work instead of sitting
[02:26] idly by while waiting for things like
[02:28] network requests and database queries
[02:31] and stuff like that. That's why async IO
[02:34] excels at what's called IObound tasks
[02:37] which are anytime your program is
[02:39] waiting for something external. Now,
[02:41] Async.io is singlethreaded and runs on a
[02:45] single process. It uses what's called
[02:47] cooperative multitasking where tasks
[02:50] voluntarily give up control. For
[02:53] CPUbound tasks that need heavy
[02:55] computation, you'd want to use processes
[02:58] instead. And we'll see how to tell the
[03:00] difference between IObound and CPUbound
[03:03] here in a bit. But before I lose your
[03:05] attention with too much theory early on,
[03:07] let's go ahead and jump into some code
[03:09] and we'll learn the basic terminology
[03:11] that we need to know as we go. So, let
[03:13] me walk through this code and explain
[03:15] the terminology as we go. Now, the
[03:17] terminology is what really trips a lot
[03:19] of people up when they first start
[03:20] learning async.io because there's quite
[03:22] a bit to remember. Now, right now we
[03:25] have some asynchronous code here that
[03:27] we'll walk through and explain, but for
[03:29] now you can see that we have a simple
[03:31] synchronous function right here at the
[03:33] top and that just sleeps for a bit and
[03:36] then it returns a string. We're running
[03:39] this synchronous function inside of our
[03:42] main function here. Our main function is
[03:46] asynchronous and you can see that it has
[03:48] this async keyword. Since it's an
[03:50] asynchronous function, we can't just
[03:53] call it directly. Uh in order to run our
[03:56] main function, we have to start what is
[03:58] called an event loop. And we're doing
[04:01] this down here at the bottom with async
[04:04] io.run and passing in uh that main async
[04:08] function there. So the event loop is
[04:10] basically the engine that runs and
[04:12] manages asynchronous functions. Think of
[04:15] it a bit as auler. It keeps track of all
[04:18] our tasks and when a task is suspended
[04:21] because it's waiting for something else
[04:24] uh control returns to the event loop
[04:26] which then finds another task to either
[04:29] start or resume. So we have to be
[04:31] running an event loop for any of our
[04:33] asynchronous code to work. That's what
[04:35] this async io.run function is
[04:37] responsible for. It's getting the event
[04:40] loop running task until they're marked
[04:42] as complete and then closing down the
[04:44] event loop whenever it's done. So let me
[04:47] go ahead and run the code that we have
[04:49] right now. And we can see that we're
[04:52] just printing out that this is a
[04:53] synchronous function. And then we get
[04:55] that result. So we came in here, ran
[04:57] this event loop. This is an asynchronous
[05:00] main function. And we are just calling
[05:02] this synchronous code here within our
[05:04] async function for now. But we don't
[05:07] want to just run synchronous functions
[05:09] inside of our event loop. We want to use
[05:11] concurrency. to use concurrency. We're
[05:14] going to be seeing this await keyword a
[05:17] lot. So, let me uncomment the futures
[05:20] code here uh so that we can talk about
[05:24] awaitables. So, let me uncomment this
[05:28] here. So, you can see here that we're
[05:30] using this await keyword. So, awaitables
[05:33] are objects that implement a special
[05:35] await method under the hood. You're
[05:38] going to see await everywhere in
[05:40] asynchronous code. an object has to be
[05:42] awaitable for us to use that keyword on
[05:45] it. Now, why can't we await a
[05:48] synchronous function uh like this sync
[05:51] function here or something like
[05:53] timesleep? Well, synchronous libraries
[05:56] don't have a mechanism to work with the
[05:58] event loop. They don't know how to yield
[06:00] control over and resume later. So,
[06:04] basically synchronous code like
[06:06] time.sleep sleep or our synchronous
[06:08] function here. Uh they don't have that
[06:11] uh underlying special await function
[06:13] that they need in order to pause their
[06:15] execution and start back later. Uh these
[06:18] things need to be coded in to be
[06:20] compatible with async io. And that's why
[06:23] we can't await time. And we need to use
[06:26] something like async io.sleep instead.
[06:30] And to use this await keyword, we also
[06:32] have to be within a function that has
[06:35] this async keyword. So if I remove async
[06:40] from our function there, now you can see
[06:42] that I'm getting a warning here. And if
[06:45] I hover over this, uh, it's not letting
[06:47] me hover right now, but we can see that
[06:49] my rough warning here is telling me that
[06:52] await should be used within an async
[06:54] function. So we have to be within an
[06:57] async function in order to use these
[07:00] await keywords. Okay. So what does await
[07:03] do? So when you await something, you're
[07:06] basically telling the event loop to
[07:08] pause the execution of the current
[07:10] function and yield control back to the
[07:13] event loop which can then run another
[07:15] task and it'll stay suspended until this
[07:19] awaitable completes. So in Python's
[07:22] async io there are three main types of
[07:24] awaitable objects. First there are co-
[07:27] routines which are created when you call
[07:29] an async function. Second there are
[07:32] tasks and tasks are wrappers around co-
[07:35] routines that are scheduled on the event
[07:38] loop. And the third there are futures
[07:40] and futures are low-level objects
[07:43] representing eventual results. Now, if
[07:46] you're coming from somewhere like the
[07:48] JavaScript world, uh futures are a lot
[07:51] like promises in JavaScript. They're a
[07:54] promise of a result that will be
[07:56] available later. But unlike JavaScript,
[07:59] in Python, we almost never work with
[08:01] futures directly. We write co- routines
[08:03] and when we schedule them as tasks,
[08:06] async.io uses futures under the hood to
[08:09] track those results, but we won't be
[08:11] seeing them much in this video. you're
[08:13] really only going to use futures
[08:15] directly if you were writing low-level
[08:17] async IO code. Uh like if you were
[08:20] building an Async compatible framework,
[08:23] but just to show you what they look
[08:24] like, let me run this example with uh
[08:27] this futures example here really quick.
[08:29] And uh just so we can see what's
[08:32] happening. So a future's job is to hold
[08:34] a certain state and result. The state
[08:37] can be pending uh meaning the future
[08:39] doesn't have any result or exception
[08:42] yet. Uh it can be cancelled if it was
[08:44] cancelled using future.canc
[08:47] uh or it can be finished and it can be
[08:50] finished uh by a result being set by uh
[08:54] future set result or it can be an
[08:57] exception with future set exception. So
[09:00] you can see here after we created this
[09:02] future and printed that out, it says
[09:04] that future was pending after we created
[09:06] it. And then we set the result right
[09:09] here to future result test and got that
[09:13] result by awaiting it. And then we
[09:15] printed that out. But like I said, this
[09:17] is lower level stuff and we won't be
[09:20] using it directly in this video. We're
[09:22] going to be working mostly with co-
[09:24] routines and tasks. So I'm going to
[09:26] delete this future example here and then
[09:29] we're going to look at co-outines and
[09:31] tasks. So let me uncomment the co-outine
[09:35] example here. And co-outines are
[09:37] functions defined with the async defaf
[09:40] keywords here. So main is a co-outine
[09:44] here. We have async defaf. If I go up
[09:46] here, we have this async function and I
[09:49] have a comment here that says also known
[09:51] as a co-ine function. So we have this
[09:53] async defaf async function. And within
[09:56] here, it's a lot like our synchronous
[09:59] function, but instead of using
[10:00] time.sleep, sleep we are using async
[10:03] io.sleep and we are awaiting that as
[10:06] well. So that's what this co-outine
[10:09] object is right here. So co- routines
[10:11] are basically functions whose execution
[10:14] we can pause and there's actually two
[10:16] terms here that we need to understand.
[10:19] There's the co-outine function which is
[10:21] what we define with the async defaf
[10:23] keywords and then there's the co-outine
[10:26] object which is the awaitable that gets
[10:29] returned when you call that function. So
[10:32] this is the co-outine function here and
[10:34] after we call that function this is the
[10:37] co-outine object here. So co- routines
[10:40] are a bit like generators in the sense
[10:42] that they can suspend execution and
[10:44] resume later but they're designed to
[10:47] work with an event loop. They have extra
[10:49] features that Async IO needs to schedule
[10:51] them, await IO, and coordinate multiple
[10:54] tasks. So, let me go ahead and run this
[10:56] here so that we can see what's
[10:58] happening. Now, again, if you're a bit
[11:00] confused right now, I think all of this
[11:02] will make a lot more sense once we look
[11:04] at the animations that I've put
[11:06] together. Uh, but right now, we're just
[11:08] focusing on learning these terms so that
[11:10] we can understand a bit better what
[11:12] we're looking at once we get to those
[11:14] animations. So you can see here when we
[11:16] executed this co-ine function, it
[11:19] doesn't run all of that function. It uh
[11:23] didn't come in here and print out that
[11:26] this was a synchronous function um
[11:29] before we got to this line here. It just
[11:32] created this co-ine object and then we
[11:34] printed out that co-ine object which is
[11:36] right here. So when we ran that co-ine
[11:39] function we got this co-outine object
[11:42] and to actually run this co-outine and
[11:45] get the result we have to await it. When
[11:48] I await that co-outine object we can see
[11:51] that that's whenever it runs the print
[11:54] statement from our asynchronous function
[11:56] and then we got that result and printed
[11:58] that out and that result was just async
[12:02] result test. Now when we await a
[12:05] co-artine object directly like this,
[12:07] it's both scheduled on the event loop
[12:10] and run to completion at the same time.
[12:13] Okay. So now let's look at tasks. So I'm
[12:16] going to comment out this co-ine section
[12:19] here. And now let's look at tasks here.
[12:24] Now tasks are wrapped co-outines that
[12:26] can be executed independently. Tasks are
[12:29] how we actually run co-outines
[12:31] concurrently. When you wrap a co-outine
[12:33] in a task using async io.create task
[12:37] like we've done here, it's handed over
[12:39] to the event loop and scheduled to run
[12:42] whenever it gets a chance. The task will
[12:45] keep track of whether the co-outine
[12:47] finished successfully, raised an error
[12:49] or got cancelled just like a future
[12:52] would. And in fact, tasks are futures
[12:54] under the hood, but with extra logic to
[12:57] actually run the co-outine and do the
[12:59] work that we want to do. That's why we
[13:01] work with tasks instead of futures uh in
[13:04] most of our code. But unlike co-outine
[13:06] objects, tasks can be scheduled on the
[13:08] event loop and just sit there without
[13:11] being run until the loop gets control.
[13:13] And this is the key to async IO. You can
[13:16] queue up multiple tasks at once and then
[13:19] the event loop will be able to run them
[13:21] whenever it's ready. Uh letting them
[13:24] take turns while waiting on IO. So let
[13:27] me go ahead and run this so we can see
[13:29] this basic example here. So you can see
[13:31] that when we uh printed out the task
[13:34] that we created here it shows that the
[13:37] task is pending. It shows us uh the name
[13:40] of the task here and the co- routine
[13:43] that it is wrapping. And when we await
[13:46] that task it runs that co- routine and
[13:48] we get those print statements and that
[13:50] returned result. Okay. So that does it
[13:53] for the terminology. Uh now let's see
[13:55] this in action with some specific
[13:57] examples and some animations. Uh I don't
[13:59] know about you all but I'm a very visual
[14:01] learner. So seeing this stuff in action
[14:04] helps me a lot more than just looking at
[14:06] the code. So first I'll show the Python
[14:09] code and then we'll see what's happening
[14:11] under the hood with an animation. So
[14:14] here is the Python code here. Now in
[14:17] this first example we're not going to be
[14:19] using async IO at all. This is just
[14:21] normal synchronous code with no event
[14:23] loop or anything like that. So we should
[14:26] know exactly how this works. So if we go
[14:30] down and uh look at the code here, we
[14:33] are resetting setting the results equal
[14:35] to this main function here. And don't
[14:37] worry, I have some extra timing
[14:39] functionality here um just so we can see
[14:42] how long this takes. Uh but then we're
[14:44] running that main function. The main
[14:46] function comes in here and we are
[14:48] running this fetch data function with a
[14:51] value of one. Fetch data then comes in
[14:54] here, prints out that we're doing
[14:56] something with one. Then it sleeps for
[14:59] that 1 second that we passed in. Then it
[15:01] prints that we're done with one and then
[15:03] returns the result of one. Okay? And
[15:06] then after that returns, that return
[15:09] value gets set to that variable there.
[15:12] Then we print that fetch one's fully
[15:14] complete. And then we come here and do
[15:16] result two is equal to fetch data two.
[15:18] That comes up here and says do something
[15:20] with two. Times sleep for two. Done with
[15:23] two then returns result of two to this
[15:27] result two here. We print out that fetch
[15:29] two is fully complete. And then we
[15:31] return both of those results. So fully
[15:34] synchronous code. We should know how
[15:36] this works. Let me go ahead and run it.
[15:38] And I added some timing code here
[15:40] that'll show us how long this took. So
[15:43] we can see it ran through that code
[15:45] synchronously and it finished in 3
[15:48] seconds. And that makes sense because
[15:50] we're doing fetch data for 1 second and
[15:53] then we're doing fetch data for 2
[15:54] seconds. So 3 seconds total. So now let
[15:59] me show this as an animation in the
[16:00] browser so that we can get an idea of
[16:02] what these animations are going to look
[16:04] like. And I've taken the timing code out
[16:07] of these animation examples so that we
[16:09] can just pay more attention to the
[16:10] actual code. Okay. So, let me go to
[16:13] example one here. Sorry about that. Now,
[16:16] hopefully this text is large enough for
[16:18] you to see. I made this as large as I
[16:20] could while everything can still fit on
[16:22] the screen here. Now, if you're walking
[16:24] through these examples, uh, these
[16:25] animations with me by using my website
[16:28] or have downloaded these yourself, then
[16:30] I've set this up so that the right arrow
[16:32] key progresses to the next steps. And
[16:35] unfortunately, I didn't add uh any
[16:37] functionality to go backwards. So if you
[16:39] want uh the animation to run again, then
[16:42] you'll have to reload the page. Okay. So
[16:45] like I said, all of this is synchronous
[16:46] code here. So we're just going to walk
[16:49] through this. It's going to uh see those
[16:51] functions there. Then we're going to uh
[16:54] run result equals main there. It's going
[16:56] to go into the main function and none of
[16:59] this is going to kick off anything on
[17:00] the event loop. We are going to run
[17:03] fetch data with the parameter of one.
[17:06] that's going to come in and run some
[17:07] print statements. We're going to do a
[17:09] time.leep. Now, time. Is going to kick
[17:12] off some background IO here and sleep
[17:14] for one second. And it's going to stay
[17:17] on this line for that entire second
[17:20] until that completes. Once that
[17:22] completes, then we can move forward and
[17:24] do our other print statements here.
[17:26] Return that. And then we're going to
[17:29] walk through do result two is equal to
[17:31] fetch data to. Same thing. We're
[17:33] printing out some uh text there. We're
[17:36] going to run this background IO. It's
[17:39] going to sleep for two seconds now. And
[17:41] that's going to stay there until that
[17:43] completes. Once that is done, then we
[17:46] can come in and print out our other
[17:49] statements. And then we return our
[17:52] result one and two. And then we come
[17:54] down here and print the results that we
[17:56] got from main, which was just uh a list
[17:59] of those two results. Okay, so that's
[18:02] synchronous code. That should be what we
[18:04] expect. But since it was synchronous, we
[18:07] were waiting around during those sleeps
[18:09] when we could have been allowing other
[18:11] code to run. So now we might want to
[18:14] improve this performance and switch over
[18:16] to using async IO. So let's move on to
[18:19] example two here and see an example of
[18:22] how someone might go about doing this.
[18:25] Now in this example, we're going to see
[18:28] what a first attempt at converting our
[18:30] code to asynchronous might look like.
[18:32] But there's going to be a common mistake
[18:34] here. Uh so you can see that we've
[18:36] converted our functions into co-
[18:39] routines. So we have uh an async defaf
[18:42] fetch data here. Our main is async defaf
[18:46] co-outine here. And then we are running
[18:49] this in an event loop here with async
[18:52] io.run.
[18:53] So this is what someone's first attempt
[18:55] might look like to go asynchronous here.
[18:58] So we are setting our tasks here
[19:01] directly to our co-outine objects here.
[19:05] Fetch data 1 and fetch data 2. Almost
[19:08] like we're just calling a function. This
[19:10] is very similar if we go back to example
[19:12] one how we were setting the result equal
[19:15] to fetch data 1 and fetch data 2. Uh
[19:17] that's what we're doing here with these
[19:19] tasks. And then we're trying to get the
[19:21] result here by awaiting those co-
[19:24] routines directly. And then after we
[19:26] await one, we're printing out that task
[19:28] one is fully complete. After we await
[19:31] two, we're printing out that task two is
[19:33] fully complete. Now, we could have
[19:35] awaited these directly. I could have
[19:36] said that result one is equal to await
[19:40] uh fetch data one like that. uh but I
[19:43] broke them up here into uh being able to
[19:47] see the co-outine object first and then
[19:50] await that separately. Okay, so let's
[19:52] run this and see if this works. So we
[19:56] run this code and we can see that it
[19:59] still takes 3 seconds. So we're not
[20:02] getting any concurrency benefit here at
[20:04] all. So why is that? Well, some people
[20:07] have a misconception that when you run a
[20:10] co-outine function like we did here that
[20:13] it creates a task and schedules it. But
[20:16] it doesn't. It just creates the
[20:18] co-outine object. So when we await that
[20:21] co-outine object, we're scheduling that
[20:24] and running it to completion at the same
[20:27] time. We get no concurrency here and no
[20:30] benefit to using async.io. So, let me
[20:33] show you what this looks like in an
[20:35] animation that I put together, and I
[20:37] think this will make more sense. So,
[20:39] here we have that same code that we had
[20:41] before. So, we're going to run through
[20:44] this. Now, once we get to results equals
[20:48] async io.run main that is going to
[20:52] create our event loop. So, now in our
[20:54] event loop, right now we have one
[20:57] co-ine. We have this main coine that is
[21:00] going to run. So now the code uh
[21:02] whenever I step forward here it's going
[21:04] to run from this co-outine here. So as I
[21:08] go through this now we're saying task is
[21:10] equal to fetch data 1 that's going to
[21:12] create a co-outine object. Task two
[21:15] that's going to be another co-outine
[21:16] object. And now when we do result one
[21:19] equal to await task one that is going to
[21:23] schedule that on our event loop and run
[21:25] it to completion at the same time. So if
[21:28] I step forward here then our main
[21:31] co-outine is suspended. So await is what
[21:36] suspended our main co-ine and now our
[21:39] event loop is looking for tasks that are
[21:42] ready. Now we just scheduled that fetch
[21:45] data one task uh whenever we ran this
[21:49] co- routine directly. So our event loop
[21:51] is going to see that and say okay I have
[21:54] a ready task here. So, let me go in here
[21:56] and run this until I hit an await. So,
[22:00] now it's going to come in here. It's
[22:02] going to print that we're doing
[22:04] something with one and then we are going
[22:06] to await that sleep. And once we hit
[22:09] that, it's going to suspend our current
[22:12] task. And it's going to uh be suspended
[22:15] until async.io.sleep
[22:18] is complete. So that's going to kick off
[22:20] our background IO here with our timer.
[22:23] And then that's going to suspend. And
[22:25] now that's going to stay suspended until
[22:28] our timer is complete. Once that timer
[22:31] is complete, then this is going to wake
[22:33] up this task and say, "Hey, this what
[22:37] you were awaiting here is complete. So
[22:39] now you're going to be ready to run
[22:41] again." So now that completed, now we're
[22:45] ready to run again. Our event loop is
[22:47] going to go through. see that we have a
[22:50] ready task here and then it's going to
[22:51] go back in and it's going to continue
[22:54] running from where it left off. So then
[22:56] we're going to go through here, print
[22:59] out these other statements and finally
[23:01] we're going to return here and once we
[23:04] return now this task is complete. this
[23:08] fetch data one task is complete and that
[23:11] is going to wake up our main co-outine
[23:13] here because now this task one that we
[23:17] were waiting for is now complete. So now
[23:21] main is ready to run again. Our event
[23:23] loop is going to see that. It's going to
[23:25] come in here and run it and now print
[23:27] out task one fully complete. And now
[23:30] we're going to do the same thing with
[23:32] await task two. It's going to suspend
[23:35] our main co-outine. It's going to uh
[23:38] schedule our task two here onto our
[23:41] event loop and also run it to
[23:44] completion. So, we're coming in here and
[23:46] we are printing these out. This await
[23:49] async io.sleep here is going to suspend
[23:53] this task until this async.io.sleep is
[23:57] done. So, it kicks off that timer. It
[24:00] suspends our task. That timer eventually
[24:03] is going to complete. Once that's
[24:05] complete, then our task can wake up
[24:07] here. And now it's ready to run. The
[24:10] event loop is going to see that. Run it
[24:13] where it left off at this await
[24:14] statement. And then we're going to walk
[24:17] through our other print statements here.
[24:19] Hit our return statement. And that's
[24:22] going to complete that task. And now
[24:25] since task two is complete, and that's
[24:27] what we were awaiting there. Now our t
[24:30] now our main co-outine is ready to run
[24:33] again. So now our event loop's going to
[24:35] see that we have a ready task here, come
[24:38] in and finish printing these out. And
[24:40] then finally it will return that main
[24:43] co- routine and close all of that down.
[24:46] And then back here in our main Python
[24:48] code, uh, we get those results and we
[24:51] can print those out. So I hope that that
[24:54] made sense. Uh, let me reload this
[24:57] really quick. I won't go through the
[24:58] entire animation again, but the one
[25:01] thing that I really want you to catch on
[25:03] to here is that whenever we created
[25:07] these co-outines here, um they are not
[25:10] scheduling any tasks on our event loop.
[25:13] They are just returning a co-outine
[25:15] object and then here when we await those
[25:18] co-outine objects directly, we are both
[25:20] scheduling them and running them to
[25:23] completion at the same time. Uh so that
[25:26] is why we are not getting concurrency.
[25:28] We only have one task down here that is
[25:30] ever running at a time. So uh basically
[25:35] we have the same performance that we had
[25:38] when we ran our synchronous code. So let
[25:41] me go back to our code here. And now
[25:43] let's look at example three. And this is
[25:46] going to be a look at one of the correct
[25:48] ways to run asynchronous code. Now the
[25:51] only thing that we've changed here from
[25:53] the previous example is that now we're
[25:56] we are creating tasks from these
[25:58] co-outines using async io.create task
[26:03] instead of just calling those co-outines
[26:05] directly. Now when we create a task it
[26:09] schedules a co-outine to run on the
[26:11] event loop. This is the part that we
[26:13] were missing from the previous example.
[26:16] So now if I run this then we can see
[26:20] that in our output that do something
[26:23] with one and do something with two ran
[26:26] one after another without the first task
[26:29] finishing and before task one was able
[26:33] to print out that it was done or uh task
[26:36] one was fully complete. It immediately
[26:39] came in here and said okay I'm going to
[26:40] do something with one. I'm going to do
[26:42] something with two. And then since our
[26:45] uh number one task only slept for 1
[26:48] second, it completed first. And then our
[26:51] second task completed um after that
[26:54] since it's sleeping for 2 seconds. And
[26:56] we can see here that the total time of
[26:58] our script here uh took 2 seconds in
[27:01] total. And that is because it ran both
[27:04] those at the same time. And that total
[27:06] time is simply how long the longest
[27:09] running task was, which was two seconds.
[27:11] So we did get concurrency here. So let's
[27:15] see this in an animation uh so that we
[27:18] can see exactly what that looks like.
[27:21] Okay. So now here we can see that I have
[27:24] the code that uh creates the task here.
[27:27] So let me walk through this and again
[27:30] we're going to get to this line here
[27:32] where we are running our event loop and
[27:34] we're going to run this main co-outine.
[27:37] So that main co- routine is now running
[27:39] on our event loop. And then when we get
[27:41] to this point here where we create this
[27:44] task with fetch data one that is going
[27:47] to schedule that task on the event loop.
[27:50] So now we can see that that task is now
[27:54] scheduled and ready on the event loop.
[27:57] Uh and now in our main co-outine here
[28:00] we're still going forward. And now with
[28:02] task two with async.io.create create
[28:04] task that is also going to schedule that
[28:07] fetch data too on our event loop. So now
[28:11] we can see that that gets scheduled as
[28:13] well and both of those are ready. And
[28:15] now when we get to this line here result
[28:18] one equals await task one. This await is
[28:23] going to yield control over to the event
[28:25] loop and it's going to suspend our main
[28:28] co-outine here until this task one is
[28:32] complete. So if we go forward with that,
[28:35] our main co- routine suspends here. And
[28:38] now our event loop is going to look for
[28:41] any ready task. It's going to see that
[28:43] we have fetch data one ready. So it's
[28:45] going to come in. It's going to run
[28:47] this. And then it's going to hit an
[28:49] await statement here. And now it's going
[28:52] to suspend uh this task until async.io.
[28:58] So it's going to kick off that async.io.
[29:01] sleep here in the background. And now
[29:03] it's going to suspend that task. Now,
[29:06] this is where we get concurrency since
[29:08] we had both of these scheduled. Now, our
[29:11] event loop is going to keep looking for
[29:13] tasks that are ready. It's going to see
[29:15] that we have this fetch data 2 here. And
[29:17] now, it's going to come in and run this.
[29:20] So, we're going to do our print
[29:21] statements here. We're going to hit our
[29:23] await, which is going to suspend our
[29:26] fetch data 2 co-ine. And it's going to
[29:28] be suspended until async.io
[29:31] sleep is done. And that's going to be
[29:33] the one for two seconds. So, it's going
[29:35] to kick that off. Then, it's going to
[29:37] suspend. And now you can see that this
[29:40] is the concurrency here. We have both of
[29:42] these timers running here in the
[29:44] background. So, these are all going to
[29:47] stay suspended until something gets
[29:50] finished. So, our first timer completes,
[29:53] it's going to wake up our first task
[29:55] here. And now that that first task is
[29:57] ready, our event loop is going to find a
[30:00] ready task. And then it's going to pick
[30:02] up where it left off and just print that
[30:03] we are done with one and then return
[30:06] that value. And now once that is
[30:09] complete, remember that our main
[30:11] co-outine is awaiting that task one. So
[30:14] as soon as this task one is complete,
[30:16] then our main co-outine is now ready to
[30:19] run again. So it's going to come in here
[30:21] and print out that task one is fully
[30:23] complete. We're going to await our task
[30:26] two. Now, task two is already suspended.
[30:29] So, there's nothing really to do here
[30:31] other than to wait for this timer to
[30:33] finish here. Once that timer is
[30:35] complete, it's going to wake up this
[30:38] fetch data 2 task here. Now, our event
[30:42] loop is going to see that that's ready,
[30:44] come in here where it left off, and
[30:46] print out that we're done with two.
[30:49] Return the results. And now that this is
[30:53] complete, our task two, it's going to
[30:56] tell our main co-outine that it's ready
[30:59] to run. Our event loop is going to see
[31:01] that and run where that left off. Print
[31:04] that task two is fully complete and
[31:07] return a list of those results. And then
[31:10] that event loop is going to close down.
[31:13] We have those results there. We can
[31:15] print those out and we have everything
[31:18] there. So I hope that that example makes
[31:21] sense and now it makes sense why uh this
[31:24] code worked here by scheduling these
[31:26] tasks ahead of time uh instead of
[31:30] whenever we awaited these co-outines
[31:32] directly because when we awaited these
[31:34] co-outines directly it didn't get
[31:36] scheduled until we hit this await
[31:38] statement. So we only had one task
[31:41] scheduled and run fully to completion
[31:44] here. Uh in our second example, we had
[31:47] both of these tasks scheduled. And then
[31:49] when we awaited, it was able to uh run
[31:53] our task one until it hit an await. And
[31:56] then once we hit that await, then it was
[31:59] able to go through and see that we had
[32:01] another task that was ready and
[32:03] scheduled and it could run that as well.
[32:05] Okay, so I hope that that makes sense.
[32:07] Uh the more examples that we see, I
[32:09] think the more clear that this is going
[32:11] to be. Now in our fourth example here,
[32:14] now I want to show you uh an example of
[32:17] something to show you something
[32:18] important about awaiting tasks and how
[32:21] things are actually run on the event
[32:23] loop here. So I haven't changed much
[32:27] with this code here. It's basically all
[32:29] the same. But all I did here uh from
[32:33] example three uh in example three I'm
[32:36] awaiting task one first and then
[32:38] printing out that task one is fully
[32:40] complete. In example four, I am setting
[32:44] result two and I'm awaiting task two
[32:47] first and printing out that task two is
[32:50] fully complete and then I am awaiting
[32:53] task one and saying that task one is
[32:55] fully complete. Now, what do you think
[32:58] is going to happen here when I run this?
[33:00] So, some people might think that we're
[33:02] going to run task two first and then
[33:05] task one or maybe run them both at the
[33:08] same time, but that task two will
[33:11] complete first. But let's see. Let's go
[33:14] ahead and run this and see what happens.
[33:17] So, when I run this, then our results
[33:20] might be a little confusing to some
[33:21] people. So, we still finished in 2
[33:24] seconds. So, it's still running
[33:26] concurrently. But if you look at the
[33:28] output, task one still runs first.
[33:31] There's no change there. The only
[33:33] difference is that it didn't move to our
[33:36] task two fully completed uh print
[33:39] statement here until task two was
[33:43] completely done. So that's what I want
[33:45] you to take away from this specific
[33:47] example is that when we await something,
[33:50] we're not guaranteeing that we run that
[33:53] particular part right at that moment. uh
[33:56] the event loop is going to run whatever
[33:58] is ready. What we are guaranteeing is
[34:01] that we're going to be done with what we
[34:04] awaited before moving on. And actually,
[34:08] it doesn't even need to be one of these
[34:10] tasks that we await. Uh so, for example,
[34:13] I could use async.io.
[34:17] Instead, and that would also yield
[34:19] control to our event loop, and those
[34:21] tasks would still run in the same order.
[34:23] Uh, and it would just wouldn't move on
[34:26] until our async io.sleep is done. So,
[34:29] let me do that and just show you what I
[34:31] mean. So, I'm going to await async.io.
[34:35] Since our longest sleep is 2 seconds,
[34:38] then I'll just sleep here for 2.5
[34:40] seconds. So, if I run this, then it's
[34:44] going to be the same output pretty much.
[34:46] Uh, except now we don't have a second
[34:49] result because await async.io. Just
[34:52] returns none. So our result two there is
[34:55] none. Uh and it finished in 2.5 seconds
[34:58] since that's now a the uh longest sleep
[35:01] that we have. But we can see that the
[35:02] order of execution basically remains the
[35:05] same. It still did something with one
[35:07] first then two got done with one got
[35:09] done with two and then once this awaited
[35:13] async iosleep was finished here that is
[35:17] when we moved on to printing out that
[35:19] task two is fully complete there. So
[35:22] that is what I wanted to show you with
[35:24] that example. Let me also show you this
[35:26] here in an animation just to really
[35:29] hammer that point home. So I'll start
[35:31] stepping through the first part of this
[35:33] pretty quickly now. Uh so we're going to
[35:35] get down to where we run our event loop
[35:38] with that main co-outine and then that
[35:40] is going to create our first task there
[35:43] with task one. It's going to schedule
[35:45] that. Our task two with create task is
[35:47] going to get scheduled as well. And this
[35:50] time we are waiting task two first. Now
[35:53] when we hit await, what it's going to do
[35:55] is it's going to suspend this coine
[35:58] until task two is done. So we're
[36:01] suspending and we're yielding control
[36:04] back over to the event loop. The event
[36:06] loop is going to go and see what tasks
[36:10] are ready. Now this uses a FIFO Q in the
[36:13] background, which is first in, first
[36:15] out. That's not super important, but uh
[36:18] uh what is important is just to know
[36:20] that um what you're awaiting isn't
[36:22] always going to be the first thing that
[36:24] gets run. It's just going to be whatever
[36:26] the event loop has ready. So right now
[36:29] it is this fetch data task one here. And
[36:32] that is going to run until it hits its
[36:35] await statement and suspends itself and
[36:38] kicks off that background sleep. And now
[36:42] we have another task ready here. The
[36:44] event loop's going to see that going to
[36:46] come in until that hits its await
[36:48] statement. It's going to kick off that
[36:49] background sleep and it's going to
[36:51] suspend itself until one of these timers
[36:54] is done. Then this timer is done here.
[36:58] It's going to wake up our first task
[37:00] here. And now this is where something a
[37:02] little different happens. Uh that was
[37:05] different than our previous example. So
[37:08] it's going to see that this is ready.
[37:09] It's going to come in and pick up where
[37:11] it left off here. We're going to print
[37:13] that we're done with one and return that
[37:15] result and that is going to complete.
[37:18] Now before we were awaiting task one
[37:20] here. So before once this task one was
[37:24] done then our main co- routine was going
[37:26] to say okay I'm ready to run again. But
[37:29] that's not what we awaited. We awaited
[37:31] task two. So what this is going to do is
[37:34] it's just going to save that result in
[37:36] memory for now. And now we still just
[37:40] have two suspended co- routines here. So
[37:43] these are going to stay suspended until
[37:45] this timer completes here. That
[37:48] completes, it's going to wake up this
[37:50] second task. And then our event loop is
[37:53] going to see that that is ready. It's
[37:55] going to come in and do its print
[37:56] statements that it's done with two. It's
[37:58] going to return that result. And now our
[38:02] main co-outine is going to get woken up
[38:04] whenever this task two is done here. So
[38:08] now it is saying that it's ready. And
[38:11] now we move forward with our print
[38:13] statements. So we're going to print that
[38:15] task two was fully completed. When we do
[38:18] this await task one here, that's already
[38:20] been completed. So there's nothing left
[38:22] to do there. All it's going to do is
[38:25] pull that result from memory that it has
[38:27] saved. So it's just going to set that
[38:30] variable equal to what that uh return
[38:33] value was. We're going to print out that
[38:34] task one is fully completed. and then
[38:37] return a list of those results. Close
[38:39] down the event loop and move through and
[38:42] print out all of those. Okay, so I hope
[38:44] that this is making more and more sense
[38:47] as we're seeing more and more examples
[38:49] here. Okay, so moving on to our next
[38:52] example. Our next example here is going
[38:54] to be really important. So let me pull
[38:57] this one up here. Now in this example,
[39:00] we're going to see what happens if we
[39:02] block the event loop with synchronous
[39:04] blocking code. So this is pretty much
[39:07] the same as the examples that we've been
[39:09] looking at where we are creating tasks,
[39:12] scheduling those on the event loop and
[39:14] then awaiting those tasks. But in our
[39:17] fetch data co-outine here, instead of
[39:19] using async io.sleep, I'm using
[39:23] time.sleep here. Now time itself isn't
[39:26] awaitable. So I can't await that here.
[39:29] If I put in await, then that's just
[39:32] going to throw an error. Um, but what we
[39:35] can do is we can run this inside of an
[39:38] asynchronous function like fetch data
[39:41] and we can schedule that on our event
[39:43] loop and we can await that co-outine.
[39:46] But this is bad practice here and we're
[39:48] going to see exactly why. Uh, because
[39:51] like I was saying before, time.leep
[39:54] isn't awaitable and it wasn't coded to
[39:56] know how to suspend itself and yield
[40:00] control over to the event loop. But what
[40:03] happens if we put that blocking call
[40:04] there and then schedule and run that co-
[40:08] routine? Well, let's go ahead and see.
[40:10] So, I'm going to go ahead and run this.
[40:12] And we can see that it did something
[40:15] with one, did something with two, uh,
[40:18] task one fully complete, task two fully
[40:20] complete, and we finished in 3 seconds
[40:22] here. So, since we finished in 3
[40:24] seconds, we know that those didn't run
[40:26] concurrently. We can also see that it
[40:28] didn't start both of these at the same
[40:30] time either. we came in and did
[40:32] something with one and it wasn't until
[40:34] we were done with one that it moved on
[40:37] to doing something with two. And that's
[40:39] because time.leep blocks the event loop.
[40:42] Now, that might not be obvious and some
[40:44] people might think that uh just because
[40:47] we had that synchronous code being run
[40:49] inside of a task that we assume that
[40:52] maybe somehow it would have worked. Um,
[40:54] but let me show you what's actually
[40:56] going on here and why this blocks. So
[40:59] I'm going to pull up our fifth example
[41:01] here and let's go ahead and run through
[41:04] this to see what happens. So I'm just
[41:06] going to go down to the part where we
[41:08] start our event loop and run that main
[41:11] co-outine there. And now within the
[41:14] event loop, we are scheduling our task
[41:17] here and creating those. And now we're
[41:21] getting to the point here where we are
[41:23] awaiting task one. So that's going to
[41:26] suspend our main co- routine and it's
[41:28] not going to pick back up on our main
[41:31] co-outine until task one is complete. So
[41:34] I'll go ahead and move forward here.
[41:36] That's going to suspend. Our event loop
[41:38] is going to find a task that is ready to
[41:40] run. It's going to come in here into
[41:42] fetch data one and it's going to run
[41:44] down through this. We're going to print
[41:46] that we're doing something with one. And
[41:48] now we're going to get to this time.
[41:51] Now here it's going to kick off that
[41:54] background IO here. But what's going on
[41:57] is that we never awaited here. So this
[42:01] um task never got suspended. So it's
[42:04] just going to sit here until this
[42:07] blocking code is done until this
[42:09] background IO is done. So what we have
[42:12] here is a blocked event loop. So
[42:15] eventually that sleep is going to
[42:17] complete. Um there's nothing to wake up
[42:19] here because we're still just um running
[42:22] synchronous code. So now that that's
[42:24] complete, we can run forward with our
[42:26] task here. Say that we're done with one.
[42:29] Return that result. Our main co-outine
[42:32] is going to wake up here. Now again,
[42:35] like I was saying before, even though
[42:36] our main co-outine is now ready because
[42:39] that task one was complete, that doesn't
[42:42] necessarily mean that that is the next
[42:44] thing that the event loop is going to
[42:46] run. Uh the event loop uses that FIFO
[42:50] first in first out Q in the background.
[42:52] And since task two has been ready for a
[42:55] while and was ready before main became
[42:58] ready again, then it's going to actually
[43:00] find this task two first. And this is
[43:03] where my animation is lacking a bit
[43:05] because it doesn't show that FIFO order
[43:07] of the ready Q. Uh but that's what's
[43:09] going on there. So it's going to see
[43:11] that our task two is ready here. We're
[43:14] going to come in and run this. We're
[43:16] going to print out a couple of things.
[43:17] We're going to get to this time. That
[43:20] timesleep is going to kick off our
[43:22] background IO. Um, but it does not know
[43:25] how to suspend itself. Uh, so it's just
[43:28] going to hang here until this sleep is
[43:30] complete. Once that sleep is complete,
[43:33] then this can continue on and print that
[43:36] we're done with that. It's going to
[43:38] return that result. Now, our main
[43:41] co-outine here has been ready for a
[43:43] while. our event loop's going to see
[43:45] that. It's going to come in and
[43:48] print out the rest of these print
[43:50] statements. Close down the event loop
[43:52] and then we will print out our results
[43:54] there. Now, let me restart this
[43:56] animation really quick. And I'm just
[43:58] going to go back to a certain part of
[44:00] this animation. It's where we completed
[44:05] our first task here. And now both of
[44:07] these tasks are ready. And like I said,
[44:09] this is a FOQ where it's going to find
[44:12] this first this uh task number two first
[44:14] and run this. Now, you might be thinking
[44:17] that I should be emphasizing the order
[44:20] in which these run a little bit more and
[44:22] kind of explain that a little bit more,
[44:25] but to be completely honest, you don't
[44:27] really want to get bogged down in what
[44:30] exactly is running on the event loop at
[44:33] any one time or what's going to be next
[44:35] or anything like that because async.io,
[44:38] So it's really meant we're going to be
[44:41] running you know tens possibly even
[44:44] hundreds of things concurrently and the
[44:46] event loop is going to handle everything
[44:48] that is ready. Uh when we run real
[44:52] asynchronous code it's not going to be
[44:55] cut and dry examples like we have here
[44:58] where we know exactly when most tasks
[45:01] will be finished and when others will be
[45:03] ready. Um, so we don't have control over
[45:06] that and we shouldn't uh want to have
[45:08] control over that. The event loop is
[45:10] just going to do its job. What we do
[45:13] have control over is not moving forward
[45:16] until something is done. So if I um, you
[45:20] know, didn't want this to move forward
[45:23] until task two was done, then I would
[45:26] put an await task two there. Um, but in
[45:30] terms of whether this goes back and runs
[45:32] the main co- routine or fetch data
[45:34] first, that shouldn't really matter that
[45:37] much to us. Uh, let the event loop run
[45:40] whatever tasks are ready. And if we
[45:44] really want to enforce uh, anything,
[45:47] it'll be that we're going to enforce
[45:49] exactly when something is done, not, you
[45:52] know, exactly whenever it gets its turn
[45:55] in the event loop. So, I hope that that
[45:57] makes sense. Um, I just kind of thought
[46:00] of that as I was walking through that
[46:02] example uh last time. Um, but with that
[46:06] said, let me go ahead and go back to our
[46:08] other examples here. So, this example
[46:11] five, the one that we just saw where we
[46:14] have this time. Uh, blocking our event
[46:17] loop, this is exactly what happens when
[46:19] we run any blocking code in our
[46:22] asynchronous functions. So, while we're
[46:24] using time.sleep asleep in this example.
[46:26] This could easily be any other code.
[46:29] This could be uh request.get making a
[46:32] web request or any other synchronous
[46:35] code. Uh because requests uh the request
[46:38] library, it's not asynchronous. So to do
[46:41] web requests asynchronously, uh we would
[46:44] need to use an asynchronous library like
[46:47] HTTPX or AIO HTTP. Uh but if we do have
[46:52] some blocking synchronous code that
[46:54] doesn't have an async IO alternative
[46:57] then we can also use async IO to pass
[47:00] this off to threads or processes and the
[47:04] event loop will manage those threads and
[47:06] processes for us and that's what we're
[47:09] going to see in the next example. So let
[47:12] me open up example six here. Now, this
[47:15] example is going to be a bit more
[47:17] advanced here, but I want to show this
[47:19] since it might be something that you'll
[47:21] see or even need to do when working with
[47:23] some asynchronous code. So, right off
[47:26] the bat, let me explain a couple of the
[47:28] changes that I made here that are
[47:29] specific to this example since we're
[47:31] using threads and processes. So, first,
[47:34] instead of running our synchronous
[47:37] blocking code inside of an asynchronous
[47:39] function, uh, which we don't want to do,
[47:42] I've instead just turned our fetch data
[47:44] function back into a regular non async
[47:48] function. So, it's just a regular
[47:50] synchronous function. And we'll use
[47:52] async.io to pass this regular function
[47:54] to a thread. And then we'll also see an
[47:57] example of how to pass this to a
[47:59] process. Now, you'll notice here that
[48:01] with these print statements, I put flush
[48:03] equal to true argument in there. Uh,
[48:06] that's just to make sure that our print
[48:08] statements come out in the order that we
[48:10] expect. Sometimes when running these
[48:12] outside of our current thread, uh, print
[48:14] statements can get buffered and come
[48:16] back in a seemingly weird order. So,
[48:18] that's more just for the tutorial here.
[48:21] Now another thing down here at the
[48:23] bottom where I am starting up our event
[48:25] loop here you'll notice that I'm also
[48:28] using this if name is equal to main
[48:32] conditional and this is for our
[48:35] multi-processing example. So when Python
[48:38] spawns multiple processes it needs to
[48:41] rerun our script in that new process. So
[48:44] uh this check makes sure that we don't
[48:46] end up in an infinite loop whenever it
[48:50] uh runs our code and spawns that new
[48:52] process. Okay. So with that said, let's
[48:54] see how we can do this here. So we still
[48:57] have our asynchronous uh main function
[49:00] here. And this is going to look very
[49:02] similar to what we were doing before. Uh
[49:05] except when we're creating our task,
[49:07] we're simply wrapping our fetch data
[49:10] function here inside of this async
[49:13] io.2thread
[49:15] function. Uh this will wrap our synch
[49:18] synchronous function with a future and
[49:20] make it awaitable. Now you'll notice
[49:22] that I didn't execute the synchronous
[49:25] function. I didn't say you know fetch
[49:27] data with parenthesis here and pass in
[49:30] that one. I don't want to do that. You
[49:32] want to pass in the function itself and
[49:35] its arguments separately uh to this
[49:38] async io.2thread function so that it can
[49:41] execute later when it's ready. Then
[49:44] we're just awaiting this just like any
[49:47] other task. Now for processes here this
[49:50] is a little bit more complicated. So
[49:52] first we have to import this process
[49:55] pool executor up here from
[49:56] concurrent.futures
[49:58] and then we have to get the running loop
[50:02] uh because we are using this
[50:05] loop.runinexecutor
[50:06] method here. So here we're just saying
[50:09] loop is equal to async.io.getrunning
[50:12] loop and then within this process pool
[50:15] executor we are creating these tasks
[50:19] with loop.runinexecutor run an executor
[50:21] passing in this process pull executor
[50:25] here. And again, just like before, we're
[50:28] passing in the function that we want to
[50:30] run in a process and the arguments
[50:32] separately. And just like with our
[50:34] threads, that's going to wrap that new
[50:36] process in a future that we can then
[50:38] await. And once we've done that, uh,
[50:41] we're simply awaiting those like we did
[50:44] before. So let me go ahead and run this
[50:47] and see what we get.
[50:50] So we can see that that works and that
[50:52] these ran concurrently. Uh it took 4
[50:55] seconds, actually a little bit over 4
[50:57] seconds because threads and processes
[50:59] have a bit of overhead to spin up and
[51:02] tear down. Now the reason it took 4
[51:04] seconds and not 2 seconds is because we
[51:07] ran these in two different groups of
[51:10] two. We ran uh both of our tasks in
[51:14] threads and then we ran both of our
[51:16] tasks in processes there. And since the
[51:18] longest task is 2 seconds, uh we ran our
[51:22] threads took 2 seconds there running
[51:24] concurrently. And then our processes
[51:26] took 2 seconds running concurrently as
[51:28] well. So just like we've been doing so
[51:30] far, uh let me pull this up here in the
[51:33] browser and run through this in an
[51:36] animation just to really knock this
[51:37] point home. Uh, this code is a little
[51:40] bit longer here. So, we can see that
[51:42] some of this gets cut off. Um, but let
[51:45] me go ahead and run through here. I'll
[51:48] scroll down to where we can see that
[51:50] we're starting up this event loop with
[51:52] that main co-outine. Okay. And now we
[51:55] come in here. We are creating this task.
[51:58] We're passing off fetch data with an
[52:00] argument of one to a thread and we're
[52:03] creating a task out of that. So, that
[52:05] gets created and scheduled. We're doing
[52:07] the same thing with fetch data and an
[52:09] argument of two there that gets
[52:12] scheduled on our event loop. And now
[52:14] when we await task one, it's going to
[52:17] suspend our main code routine here until
[52:20] that task one is complete. So now it's
[52:23] going to find our thread here. Now I
[52:25] don't have the code here for our thread.
[52:28] And the reason I'm not showing our
[52:31] synchronous code in this task is because
[52:33] that code isn't running in our current
[52:35] thread anymore. Uh that's going to go
[52:38] off and run in its own thread. So
[52:41] eventually uh what that's going to do is
[52:43] that task is going to hit and await um
[52:47] something that this async io.2 thread
[52:50] puts into place for us. And then it's
[52:52] going to kick off that background thread
[52:54] and run our synchronous code for us. So
[52:57] this thread gets started here running
[52:59] that synchronous fetch data code. It's
[53:02] going to suspend that task and then we
[53:05] might see some print statements coming
[53:07] in here while that thread is running
[53:09] that synchronous code. But now our event
[53:12] loop is free to move on to our other
[53:15] task here. It's going to run our other
[53:18] bit of synchronous code there in another
[53:20] thread and suspend this second task
[53:23] here. And now both of these threads are
[53:25] going to be running in the background.
[53:27] And then eventually that is going to
[53:30] complete. This thread is going to be
[53:32] done. It's going to notify our twothread
[53:36] task here. And that's now going to be
[53:38] ready. Once that is ready, then it's
[53:41] going to return and complete. And then
[53:44] it's going to wake up our main co-
[53:46] routine here. Since we were awaiting
[53:47] that task one, it's going to print that
[53:50] that's fully completed. We're going to
[53:52] move on to waiting for that task two
[53:53] thread to be done. So again, we've seen
[53:56] all this before. That completes. That's
[53:59] ready. That completes. That's ready. So
[54:04] a lot we're doing a lot of the same
[54:06] stuff here. So I'm going to keep going
[54:08] through this a little bit faster now
[54:10] because we're kind of should be kind of
[54:12] used to this as we go. Now we're
[54:14] awaiting task one. Task one's going to
[54:16] come in here. Run. That's going to be a
[54:18] process in the background IO here. It's
[54:21] still printing stuff out out here. We're
[54:24] running another process in the
[54:25] background IO. Eventually, that's going
[54:28] to complete.
[54:30] That's going to complete there. Our main
[54:33] co-outine is going to pick back up, do
[54:35] some print uh do some print outs there,
[54:38] wait for that second task to finish
[54:40] there. Once it finishes, it wraps up and
[54:43] completes. And then we move on with
[54:46] printing out all of our code here. Now,
[54:49] I know that that example was a bit more
[54:51] complicated, but I wanted to show that
[54:53] because if we're using async io, there
[54:55] might be times when we don't have an
[54:57] asynchronous option in order to get
[54:59] concurrency, and we'll need to run some
[55:02] blocking code in threads or processes.
[55:05] Uh, but now let's go back to our more
[55:08] standard use cases here. So this is our
[55:11] last example here before I get on to a
[55:14] uh real world example and we can uh see
[55:16] how to update a real codebase to
[55:19] asynchronous. So with example seven here
[55:23] in this example we're going to see other
[55:25] ways that we can schedule and await
[55:27] tasks. So so far we've been creating
[55:30] tasks and uh one at a time and awaiting
[55:33] them manually. But a lot of times we
[55:36] might want to create a bunch of tasks
[55:38] and run them all at once. We can do this
[55:40] with either gather or with task groups.
[55:44] So here our first section here uh I've
[55:48] just taken out some print statements
[55:50] along the way. But our first section
[55:52] here is basically what we've been doing.
[55:54] We're creating these tasks manually.
[55:56] They get scheduled then we await them
[55:58] manually and get those results. Now our
[56:01] next example here uh what we're doing is
[56:04] we are creating and awaiting a bunch of
[56:08] co-outine objects in a list and then we
[56:11] are awaiting those with async io.gather.
[56:15] So what I have here is just a list
[56:17] comprehension. We're saying that we want
[56:19] fetch data for i in range of 1 to three.
[56:23] So that's still only going to give us
[56:24] two there. Fetch data one and fetch data
[56:27] two. And then we are passing those in to
[56:30] async io.gatherather. And then we are
[56:33] awaiting that gather. And I'm going to
[56:35] go over these in more detail in just a
[56:37] second. But let's move on to look at
[56:40] this task group here as well. Oh, I'm
[56:42] sorry. This isn't the task group. Uh
[56:44] this is another gather here. Um in this
[56:47] one instead of creating a list of
[56:49] co-ines like we did up here in this one
[56:51] we are creating a list of tasks and
[56:54] those tasks are just wrapping those
[56:56] co-ines there we can gather those as
[56:59] well. So I here I have a list of co-
[57:02] routines that I'm passing in to gather.
[57:04] Here I have a list of tasks from those
[57:06] co- routines that I'm passing in to
[57:08] gather. um down here with the task
[57:11] group. With the task group here, I'm
[57:13] just saying uh task group as TG and then
[57:16] within the task group, we're doing a
[57:19] list comprehension here of tg.create
[57:23] task on that uh co- routine there and
[57:26] we're getting those results. Now, we're
[57:28] going to look at all these more in depth
[57:30] here in just a second, but let me go
[57:32] ahead and run these and we can see the
[57:35] output here before we go over this code
[57:38] a little further. Okay, so this took 8
[57:40] seconds total. Now, these did all run
[57:42] concurrently. We ran these in four
[57:45] different groups and each group took 2
[57:46] seconds. So, that's why it took 8
[57:49] seconds total. 2 seconds for four
[57:51] groups. Now, we've already seen our
[57:54] manual task creation up here plenty of
[57:56] times so far. So let's skip over that
[57:59] and let's go to our async.io.ather here.
[58:02] Now when it comes to gather, you'll
[58:04] notice that these asteris that I'm using
[58:07] here before our list. Now what this is
[58:10] doing is it's unpacking our list. Uh now
[58:15] gather doesn't take a list as an
[58:17] argument. So unpacking it with this
[58:19] asterisk is basically the same as
[58:21] passing all of these u items from this
[58:24] list in individually. Now for these two
[58:27] gather examples here, one of them I'm
[58:30] passing in co- routines directly and the
[58:32] other one I'm passing in tasks. Now you
[58:34] can pass in co- routines directly if you
[58:37] just want to get the results. Uh but
[58:39] remember that tasks add some extra
[58:41] functionality. So if you want to monitor
[58:44] or interact with the tasks in any way
[58:46] before they complete then you'd want to
[58:48] use tasks. But if you just want the
[58:50] results then it's fine to just pass in
[58:52] pass in uh that list of co-ines. Now for
[58:56] our task group here, uh this is the
[58:58] first time that we've seen an async
[59:01] context manager. Now just like
[59:03] functions, context managers can be async
[59:06] when they need to do IO operations
[59:08] during setup or tearown. That's why we
[59:11] have async with here. So task group does
[59:15] a lot of async work for us uh when
[59:17] entering and when exiting. So, it tracks
[59:20] tasks, it waits for completions, uh,
[59:22] handles cancellations, handles errors,
[59:25] stuff like that. Now, the main thing
[59:26] that you'll notice is that we're not
[59:29] awaiting anything with the task group.
[59:32] We're not awaiting these results
[59:34] anywhere, and we're not awaiting them
[59:36] once we get out of that context manager
[59:39] either. It awaits all the tasks that we
[59:41] create for that task group for us when
[59:43] it exits this context manager. And we'll
[59:46] see another example of an async context
[59:48] manager here in a bit. Uh that isn't a
[59:51] task group. Um they can be used for
[59:53] things that need to set up and tear down
[59:56] uh you know for network requests, file
[59:58] access, database operations, all kinds
[1:00:01] of stuff. Now you might be wondering
[1:00:03] which ones you use here. Should you use
[1:00:06] gather for a bunch of different tasks or
[1:00:08] should you use a task group? Now, I tend
[1:00:11] to use task groups a lot of the time,
[1:00:14] but basically the key difference between
[1:00:16] gather and task groups is how they
[1:00:18] handle errors. So, with async io.gather,
[1:00:22] if you use the default of return
[1:00:26] exceptions is equal to false, which I
[1:00:29] honestly wouldn't really recommend. And
[1:00:32] as a matter of fact, I didn't realize
[1:00:34] that I uh didn't have return exceptions
[1:00:38] equal to true here in gather. Um so I
[1:00:41] would always recommend if you're going
[1:00:43] to use gather to use this return
[1:00:45] exceptions equal to true. The default is
[1:00:47] false. But with that set to false, if
[1:00:51] one task fails, then it raises the first
[1:00:54] exception that it saw. Uh you don't get
[1:00:56] a bundle of errors or any of the
[1:00:59] successful tasks. And if it fails, other
[1:01:02] tasks won't be cancelled. So you risk
[1:01:04] having orphaned task. Task group, it
[1:01:08] also fails quickly, but it gives better
[1:01:11] a uh better errors and handles cleanups
[1:01:14] a bit better. So I wouldn't really use
[1:01:16] gather with its default argument of
[1:01:18] return exceptions equal to false. But it
[1:01:21] does have a good use case for return
[1:01:23] exceptions equal to true because if any
[1:01:25] task does fail with return exceptions
[1:01:28] equal to true, it still runs the other
[1:01:31] task for you. Every awaitable in that
[1:01:33] gather finishes whether it succeeds or
[1:01:36] fails. So then your result is just a
[1:01:39] list where each position is either the
[1:01:42] result or uh of the success or the
[1:01:45] exception of the failures. Um, this is
[1:01:48] what you want to use if you want to run
[1:01:51] all the tasks even if some error out.
[1:01:54] Like maybe you have a bunch of URLs that
[1:01:56] you want to crawl. Uh, but you don't
[1:01:58] want all of them to fail if only one of
[1:02:01] those URLs fails and gets hung up or
[1:02:04] doesn't exist or something like that.
[1:02:06] For that, you would use async.io.
[1:02:09] Now, with async.io.task task group here
[1:02:12] it also on the first failure uh it
[1:02:15] cancels all the other tasks. So if it
[1:02:18] fails it raises an exception group
[1:02:21] containing all exceptions from the
[1:02:23] failed tasks and that would include
[1:02:25] exceptions from canceled tasks as well.
[1:02:28] Now there's no option to keep running
[1:02:30] other tasks after one fails. So we use
[1:02:34] this when we want all our tasks to run
[1:02:36] successfully. So basically to sum all
[1:02:39] that up, if you want tasks to continue
[1:02:42] running even if some fail, then you
[1:02:45] would use gather with return exceptions
[1:02:48] equal to true. Now, but if you want all
[1:02:51] of the tasks to either fail together or
[1:02:53] succeed together, then you would use a
[1:02:56] task group. And I almost never would
[1:03:00] recommend using uh gather with the
[1:03:02] default of return exceptions equal to
[1:03:05] false. If somebody knows of some edge
[1:03:07] cases that I'm not thinking of, then
[1:03:08] feel free to uh leave me a comment. But
[1:03:12] um you know, if that's the case, I would
[1:03:14] uh if you want it to fail fast, then I
[1:03:16] almost always use a task group instead.
[1:03:19] Okay, so just like with the other
[1:03:21] examples, let's look at this example in
[1:03:23] the browser here to see what's going on
[1:03:25] under the hood. Uh but I'm going to run
[1:03:27] through this animation a little faster
[1:03:30] because it's going to be very similar to
[1:03:32] the examples that we've already seen. Uh
[1:03:34] the tasks are going to be scheduled and
[1:03:37] awaited in different ways than we've
[1:03:39] seen before, but the behaviors are
[1:03:41] basically the same. Um so let's step
[1:03:44] through this pretty quickly here. We're
[1:03:46] creating a task. It gets scheduled.
[1:03:48] Creating another task that gets
[1:03:50] scheduled. We are awaiting our main co
[1:03:53] routine here and suspending it. Running
[1:03:55] our first task that suspends. Our second
[1:03:59] task suspends after we kick off both of
[1:04:02] these background IO tasks here. And
[1:04:05] actually, so I'm not scrolling here, let
[1:04:08] me make the screen a little bit smaller.
[1:04:10] I know that's going to be harder to
[1:04:11] read. Uh, but basically, we just want to
[1:04:14] see these animations anyway. Um, the
[1:04:16] code is the same as it was in our
[1:04:18] example here. Um, but
[1:04:22] this is the examples that we've seen
[1:04:24] already where we're creating these
[1:04:26] manually. That's why I'm stepping
[1:04:27] through these pretty quickly. Okay, so
[1:04:29] now we are getting to our gather
[1:04:32] examples. So first we're creating this
[1:04:34] list of co-outines here and that's not
[1:04:38] going to schedule anything on our event
[1:04:40] loop and now we are here where we are
[1:04:44] gathering all of those and we are
[1:04:47] awaiting that gather. Now since these
[1:04:49] are co-ines these are going to get
[1:04:51] scheduled and run to completion at the
[1:04:54] same time. So that is going to suspend
[1:04:56] our main co-ine and schedule all of
[1:05:00] those uh co-ines that were in this list.
[1:05:04] So now just like we saw before, whoops,
[1:05:07] let me get that there. Uh just like
[1:05:10] before, it's going to run through since
[1:05:12] we have both of these scheduled on our
[1:05:14] event loop. It's going to run these
[1:05:16] concurrently until these complete and
[1:05:20] return and complete and return. And once
[1:05:24] all of those are done, then it's going
[1:05:26] to uh tell our main co- routine here
[1:05:29] that it's ready and it's going to go
[1:05:31] ahead and move forward. Now with our
[1:05:34] gather task, this is a little bit uh
[1:05:37] different because we are creating tasks
[1:05:39] here in a list instead of creating co-
[1:05:42] routines in a list. Now remember when we
[1:05:44] create a task, it's going to go ahead
[1:05:46] and schedule those. So those get
[1:05:49] scheduled when we create those tasks and
[1:05:52] then when we await those uh that is
[1:05:56] whenever we are going to suspend our
[1:05:59] main co-outine and our event loop can
[1:06:01] actually get around to running those.
[1:06:04] But those are going to run concurrently
[1:06:06] like we've seen before.
[1:06:09] And once those are done, then our await
[1:06:12] is going to go ahead and wake up our
[1:06:15] main co-outine here and go ahead and
[1:06:18] move forward. And then with our task
[1:06:21] group, we're going to come in here to
[1:06:23] our task group. We are going to create
[1:06:26] these tasks in our task group. So those
[1:06:28] are going to get scheduled onto our
[1:06:30] event loop. Now there is no await
[1:06:32] statement here. This automatically
[1:06:35] awaits whenever it exits our context
[1:06:39] manager here. So once that context
[1:06:42] manager ends, that's when we suspend our
[1:06:45] co- routine there and then our event
[1:06:47] loop can find these ready tasks and run
[1:06:50] these.
[1:06:52] So just like before, these are run
[1:06:55] concurrently.
[1:06:57] And once those are done, that's whenever
[1:06:59] it can fully exit that um task group
[1:07:03] context manager there and move on to our
[1:07:06] print statement that it has those task
[1:07:09] group results. So then we are finished
[1:07:11] up with our main co-outine here that
[1:07:14] completes down here at the bottom. We
[1:07:17] are free to move on and print out our
[1:07:20] results. Okay, so that is the last
[1:07:22] animation and that is the last of our
[1:07:24] code examples. Now we can look at a
[1:07:27] realworld example here. I hope that
[1:07:29] those animations really helped visualize
[1:07:31] what's going on in the background and
[1:07:33] helped make it easier to understand how
[1:07:35] Async IO works. Um, with my brain, I can
[1:07:38] kind of understand stuff better when
[1:07:40] it's visual like that and interactive
[1:07:42] like that. Um, but now let's look at a
[1:07:45] real world example and we'll see how to
[1:07:47] convert this real world example of some
[1:07:49] synchronous code and convert this over
[1:07:52] to using async IO. So, let me open up
[1:07:55] this real world example here. And first,
[1:07:58] we'll just uh look over what this does.
[1:08:01] So, I have a synchronous script here
[1:08:02] that downloads some images and then it
[1:08:04] processes those images. So, let me walk
[1:08:07] through this and explain what's going
[1:08:09] on. So, I have my imports up here. I
[1:08:12] have my image URLs. This is just 12 uh
[1:08:15] highdefinition pictures here. And then I
[1:08:18] have a download single image function,
[1:08:21] download images function that loops over
[1:08:24] uh those URLs and downloads the single
[1:08:26] images for each of those URLs. We have a
[1:08:29] process single image here. And I just
[1:08:32] grabbed this offline. This is uh an
[1:08:34] algorithm that does edge detection on
[1:08:37] photos. So it's just some photo
[1:08:39] processing there. Um, we have a process
[1:08:42] images here that loops over all of the
[1:08:45] images that we need to process that we
[1:08:47] downloaded and processes one at a time.
[1:08:50] And then we have our main method here.
[1:08:53] And in our main method, uh, just ignore
[1:08:56] I have a bunch of timing uh, stuff here
[1:08:59] just to see how long this script takes
[1:09:01] to run. But the big part is that we have
[1:09:03] our image paths where we are downloading
[1:09:05] all of our images. And then we have our
[1:09:07] process paths where we are processing
[1:09:10] all of those images that we downloaded.
[1:09:12] And then I have some final wrapping up
[1:09:15] here with the timing. And then I'm
[1:09:17] printing out um all the images that we
[1:09:19] download and how long everything took.
[1:09:22] And then finally down here at the bottom
[1:09:24] I am running that main function. So, let
[1:09:27] me go ahead and run this and we'll see
[1:09:30] how long this synchronous script takes.
[1:09:33] So, we can see that we are downloading
[1:09:35] some images here and I might fast
[1:09:38] forward a little bit until this is done.
[1:09:41] Okay, so I fast forwarded a bit there uh
[1:09:43] just so we didn't have to watch all of
[1:09:45] these images download and process. But
[1:09:47] at the bottom, we can see how long this
[1:09:50] took. Um, I'm actually going to copy
[1:09:53] this uh so that we can uh reference this
[1:09:56] later. I'm going to paste it into some
[1:09:58] of my notes here off screen. But we can
[1:10:00] see that the total execution time here
[1:10:02] was about 23 seconds. Um, the time that
[1:10:06] it took to download the images was 13
[1:10:09] seconds, which was 5.5% of the total
[1:10:12] time when we processed our images in
[1:10:15] about 10 to 11 seconds, which was about
[1:10:17] 44% of the time. So, we're going to be
[1:10:19] able to speed this up by a lot. Uh, but
[1:10:22] I don't know about you, but when I was
[1:10:24] first learning async.io, I didn't even
[1:10:26] really know where to start with, uh, you
[1:10:29] know, in terms of where to make changes
[1:10:30] to the synchronous script. Uh, which
[1:10:33] parts do I make asynchronous? Do I use
[1:10:36] threads or do I use processes? How do I
[1:10:38] know? Well, here are some tips that I
[1:10:40] can give uh to give you an idea of where
[1:10:43] to start. So first we need to determine
[1:10:46] what's IObound and what's CPUbound. Like
[1:10:50] I said before, IObound work is where
[1:10:52] we're just waiting around on external
[1:10:54] things to be done like web request,
[1:10:57] database access, uh file access, things
[1:11:00] like that. It's often easy to guess
[1:11:03] what's IObound in your code. Uh if you
[1:11:06] know what you're looking for, you can
[1:11:07] even look for certain words. You know,
[1:11:09] words like fetch or get or web request
[1:11:13] or database. Um those usually lean
[1:11:16] towards IObound type of things. Uh
[1:11:19] certain words like compute or calculate
[1:11:22] or process, those usually lean towards
[1:11:25] CPUbound type of tasks. Um now once you
[1:11:29] work with this stuff more and more,
[1:11:30] that's going to become more intuitive.
[1:11:33] But if you want to be absolutely sure,
[1:11:36] we can use actual profiling tools to see
[1:11:39] where we're spending the most of our
[1:11:40] time in our code. So I'm going to show a
[1:11:43] profiling example using scaline, which
[1:11:46] is a really nice profiler that I like.
[1:11:48] And we're not even going to need to add
[1:11:50] anything extra to our code. So first I'm
[1:11:54] going to install uh scaling here. And
[1:11:57] you can install it with pip or UV. I'm
[1:12:00] going to use UV here. Uh, so you could
[1:12:02] do a pip install scaline. I'm going to
[1:12:05] use uv and do a uvad scaline. I think I
[1:12:09] spelled that right. And once that's
[1:12:11] installed, I'm going to run a command
[1:12:13] that will profile our code. Now, I have
[1:12:16] some snippets uh open here that I'm
[1:12:19] going to use to copy this command in uh
[1:12:22] just so I don't mistype anything here.
[1:12:25] So, let me copy this and then paste this
[1:12:29] in. Now since I'm using UV here, I'm
[1:12:32] doing a UV run-m you can all you also
[1:12:35] use a python-m
[1:12:37] there. Um, so I'm running this scaline
[1:12:40] module. I'm creating an HTML report here
[1:12:44] called profile report and we are going
[1:12:47] to profile uh my script here is called
[1:12:50] real world example sync v1. So, I'm
[1:12:53] going to run that, and it's going to
[1:12:55] rerun our script where it's going to
[1:12:57] download uh those images and process
[1:13:00] those images again, but it's going to
[1:13:02] profile it as it goes and give us an
[1:13:04] idea of where we're spending most of
[1:13:06] that time. And once this is done, I will
[1:13:09] go ahead and open this profile
[1:13:11] report.html in my browser. Okay, so that
[1:13:15] script finished. Uh it actually took a
[1:13:17] good bit longer that time. That time it
[1:13:19] took 29 seconds. Uh the last time it
[1:13:21] only took 23 of the 24 seconds. Um but
[1:13:25] let me go ahead and open this profile
[1:13:29] report that it gave us here in the
[1:13:32] browser. I will allow that. Okay. So
[1:13:35] here's our report. Let me make this uh a
[1:13:38] lot larger here so that we can uh so
[1:13:41] that you can read this. Okay. So we can
[1:13:44] see here that it breaks up the time here
[1:13:47] between Python time, native time and
[1:13:50] system time. Now the system time is
[1:13:53] likely a lot of waiting on IO here. And
[1:13:57] we can see that most of our system time
[1:14:01] is here in this download single image
[1:14:05] function here. So that is a great
[1:14:07] indicator that we can speed up uh this
[1:14:10] download single image with async io or
[1:14:13] threads if you want to use threads also.
[1:14:16] Now a lot of the time being spent in
[1:14:18] python is here within our process single
[1:14:22] image function. So that likely means
[1:14:25] that that is CPUbound. Um so we'd get
[1:14:28] more of a speed up using multiple
[1:14:30] processes on that than we would from
[1:14:32] async IO or threads. So now that we've
[1:14:36] profiled this, we have some actionable
[1:14:38] knowledge of where we can speed up our
[1:14:40] code. So let's go change these specific
[1:14:43] functions. So I'm going to go back to
[1:14:45] our Python code here. I'm going to close
[1:14:47] down our output there. And now let's go
[1:14:50] to the top and let's import async IO.
[1:14:55] Um, now for this first example, let's
[1:14:59] assume that I don't know about any
[1:15:02] asynchronous libraries that we can use
[1:15:05] to do these web requests. So, there are
[1:15:07] asynchronous libraries out there that
[1:15:09] make this easier like HTTPX or AIO HTTP.
[1:15:14] But let's say that for now I just wanted
[1:15:17] to keep using uh requests. We can see
[1:15:19] here that I'm using um a request session
[1:15:23] here and I'm using a session.get with
[1:15:26] that request library. Now, like I've
[1:15:28] been saying, request isn't asynchronous.
[1:15:31] So, to continue using requests, we
[1:15:33] really don't have any choice other than
[1:15:35] to use threads. Uh we saw how to do this
[1:15:38] earlier. So, let's go ahead and change
[1:15:40] this to use threads and have Async.io
[1:15:44] manage those threads. So, I'm going to
[1:15:46] go up to our download single image here.
[1:15:48] Now, the first thing I'm going to do, I
[1:15:50] don't know if these sessions are thread
[1:15:52] safe, so I'm going to remove this
[1:15:56] session as an argument here, and I'm
[1:15:59] just going to use request.git instead of
[1:16:03] session.git there. Um, that'll have to
[1:16:07] create a new session every time for each
[1:16:10] of those URLs. Uh, but that's okay. But
[1:16:12] other than that one small little change,
[1:16:15] since we're going to use threads for
[1:16:16] now, I'm not going to touch this
[1:16:18] function anymore. I'm going to leave
[1:16:20] this as a regular synchronous function.
[1:16:23] Um, I'm going to go to where it's
[1:16:24] calling this function to make the
[1:16:26] changes that we need. And it's calling
[1:16:28] this function in our download images
[1:16:31] function here. So within download
[1:16:33] images, this is where we'll use async.io
[1:16:36] to send this off to threads. So that
[1:16:38] means that we're going to convert this
[1:16:40] to a co-outine. And to do that, the
[1:16:42] first thing that we need to do is add
[1:16:44] async before the function here. And I'm
[1:16:48] no longer using uh sessions since I
[1:16:50] wasn't sure if that was thread safe. So
[1:16:53] I'm going to take out that context
[1:16:55] manager there and unindent those. And
[1:16:59] I'm no longer passing in that session as
[1:17:02] an argument there. Okay. So now we're
[1:17:04] running this single download single
[1:17:06] image function for every URL in our URLs
[1:17:09] list. So instead, let's send that off to
[1:17:13] a thread. To do this, we can use gather
[1:17:16] or we can use a task group. I'm going to
[1:17:18] use a task group. And I'm actually going
[1:17:20] to grab a little bit of code from my
[1:17:22] snippets file here. And I'll paste this
[1:17:24] in above uh what we have now so that we
[1:17:27] can see the difference. I just don't
[1:17:29] want to accidentally uh mistype anything
[1:17:32] and have to do a bunch of debugging in
[1:17:35] the middle of a tutorial. So let me just
[1:17:39] copy this from my snippets here. And so
[1:17:43] this is what we had before. And this is
[1:17:46] what we have now with this task group.
[1:17:49] So you can see here that we are creating
[1:17:52] our task group here. And within here
[1:17:54] we're creating a bunch of tasks. And
[1:17:56] we're saying task groupoup.create task.
[1:17:58] And now instead of running this download
[1:18:02] single image directly, we're now sending
[1:18:05] this off to an uh using async io.2
[1:18:09] thread. And this create task method here
[1:18:12] is going to create tasks uh for us of
[1:18:15] all those threads uh that we can await.
[1:18:18] Now like we saw before, this task group
[1:18:20] is going to await on its own after it
[1:18:23] exits this context manager here. So
[1:18:26] outside of this context manager uh to
[1:18:28] get the results from all of those tasks,
[1:18:32] I can just create a new list here and
[1:18:34] say task.result for task in task. So now
[1:18:37] that we have that, I'm going to go ahead
[1:18:39] and just remove what we had before and
[1:18:42] use this new way of doing it here and
[1:18:44] save that. And now just to show that
[1:18:46] it's important to know when to use
[1:18:48] async.io versus threads versus processes
[1:18:51] correctly. Let me also shove our image
[1:18:55] processing off into threads. Also, uh
[1:18:58] judging from our profiling that we saw
[1:19:00] earlier, our processing should be
[1:19:02] CPUbound. So, we shouldn't get any speed
[1:19:05] up by uh shoving these onto threads. And
[1:19:09] I want to be able to show that. So, let
[1:19:11] me go ahead and put those in threads
[1:19:13] just so we can show uh that it we don't
[1:19:16] get a speed up there. Um, so I'm going
[1:19:18] to go down to our process images
[1:19:20] function here. And I'm also going to
[1:19:23] make this a co- routine as well. And
[1:19:26] just like we did for our downloads, let
[1:19:28] me go ahead and grab a small snippet
[1:19:30] here. This is going to be very similar
[1:19:33] uh to our downloads code here. Again,
[1:19:36] I'll put it right above what we had
[1:19:39] before, but we are basically doing the
[1:19:42] same thing here. We're creating a task
[1:19:43] group. within that task group, we're
[1:19:45] creating a bunch of tasks. And what
[1:19:48] those tasks are uh is we are uh passing
[1:19:52] this process single image here off to a
[1:19:55] thread and it's going to do that for
[1:19:58] every image that we have in our list of
[1:20:01] images. And then once we exit that
[1:20:03] context manager, uh we are just getting
[1:20:07] a another list comprehension here uh
[1:20:09] setting the task. for every task in that
[1:20:12] task list. Okay. So now I'm going to uh
[1:20:16] remove the old way of doing that there.
[1:20:19] Now I can't run this quite yet. Uh since
[1:20:22] we converted these to co- routines, we
[1:20:24] now need to await these and also run an
[1:20:27] event loop. Uh so we're calling these
[1:20:30] process images and this uh download
[1:20:34] images here. We are calling those within
[1:20:36] our main function here. Uh so within our
[1:20:40] main function, I'm also going to convert
[1:20:43] this to an async function because I
[1:20:45] can't await those uh if I'm not in an
[1:20:48] async function. And now that we are in
[1:20:52] uh an async function here, instead of
[1:20:54] setting our image paths equal to
[1:20:56] download images, I'm instead going to
[1:20:59] await download images since that's now a
[1:21:02] co-outine. And I'm also going to await
[1:21:05] process images as well. And lastly uh
[1:21:09] where we are running our main function
[1:21:11] down here. This is now our main function
[1:21:14] is now the main entry point for our
[1:21:16] asynchronous code. So we need to run
[1:21:18] that in an event loop. So to do that I'm
[1:21:21] going to say async io.run
[1:21:25] and I'm going to pass that in to run.
[1:21:28] Okay. So hopefully I typed everything
[1:21:31] correctly there. Um, now let me rerun
[1:21:34] this and let's see uh if we're able to
[1:21:36] get a speed up by passing all of those
[1:21:39] off to threads. So we can see that it
[1:21:41] kicked off a lot of downloads uh really
[1:21:44] quickly and downloaded those. Uh but the
[1:21:47] processed images, it's still taking a
[1:21:50] while here. And you can see with threads
[1:21:53] some of this uh output gets buffered a
[1:21:55] little weird and puts them on the same
[1:21:57] lines. But that's okay. We're not too
[1:21:59] concerned about uh this uh weird output
[1:22:01] up here. That's just showing us what
[1:22:03] those threads were printing out with the
[1:22:05] process images and stuff. This is mainly
[1:22:07] what we're concerned with here is how
[1:22:10] long it took. So, right off the bat, you
[1:22:13] can see that it sped up these downloads
[1:22:15] by a lot. Uh they all ran in their own
[1:22:18] threads concurrently. Um, before I wrote
[1:22:21] this down here, before our download of
[1:22:23] 12 images took 13 seconds and now it's
[1:22:27] taking 2 seconds. And our process images
[1:22:30] last time took 10.49 seconds. Now it
[1:22:34] took 10.63.
[1:22:36] So it sped up our downloads by a lot,
[1:22:39] but it didn't speed up our processing,
[1:22:41] which we expected. Um, so that's good.
[1:22:44] So it's showing us that we sped up our
[1:22:46] IObound code but not our CPUbound code.
[1:22:49] Okay. So now let's take another look at
[1:22:52] how we can improve this code. So right
[1:22:54] now we've just shoved everything off to
[1:22:56] threads. But you really only want to do
[1:22:59] that when there's no asynchronous
[1:23:01] alternative. But for rew uh requests we
[1:23:04] do have async.io compatible libraries.
[1:23:06] Uh the two most popular are HTTPX and
[1:23:10] AIO HTTP. I'm going to use HTTPX in this
[1:23:15] example uh because it's more similar to
[1:23:17] requests. And we've also got some file
[1:23:20] reading and writing that we can pass off
[1:23:22] to an asynchronous library as well. And
[1:23:25] for that we can use a io files package.
[1:23:29] So let me go ahead and install these.
[1:23:31] I'll open up our terminal again here. Um
[1:23:35] now just like before you can use pip
[1:23:37] install or I'm using uv so I'm going to
[1:23:40] add these with uv. So, I'm going to use
[1:23:43] httpx
[1:23:44] for the web requests and I'm going to
[1:23:48] use um a io files for the uh file IO.
[1:23:55] So, now that we have those installed, uh
[1:23:57] let's go up here and let's update our
[1:24:00] imports. So, I'm going to uh import
[1:24:05] httpx.
[1:24:07] I'm also going to import a io files and
[1:24:13] I'm no longer going to use requests. So
[1:24:15] I'm going to get rid of requests there.
[1:24:18] And also for our image processing, we
[1:24:20] saw that the threads didn't speed that
[1:24:22] up, which we knew was going to happen.
[1:24:24] Um, but I wanted to show how someone
[1:24:26] might try that anyway. Uh, but instead,
[1:24:30] uh, let's pass those off to processes.
[1:24:32] So adding additional processes for that
[1:24:34] work is how we speed up CPUbound tasks.
[1:24:37] So to do this I'm going to import the
[1:24:40] process pool executor from uh
[1:24:42] concurrent.futures.
[1:24:44] So that's from concurrent.
[1:24:46] Futures and we are going to import the
[1:24:50] process pool executor. Okay. It
[1:24:53] automatically u sorts my imports there.
[1:24:56] So if those jump around, don't worry
[1:24:58] about that. Okay. So now let's turn our
[1:25:00] download single image uh function here
[1:25:04] into an asynchronous function that uses
[1:25:06] httpx.
[1:25:08] Uh but first I'm going to update the
[1:25:10] download images function here first. Um
[1:25:13] and again I'm going to grab this from my
[1:25:16] snippets file and I'll explain what we
[1:25:19] changed. Uh just so I don't mistype
[1:25:21] anything here. Let me grab this from my
[1:25:25] snippets and paste this in. and then
[1:25:28] I'll explain what we changed here. Now,
[1:25:32] if you remember before I passed this off
[1:25:34] to threads with the request library, I
[1:25:37] was doing something like this with
[1:25:38] requests. I was do using a request
[1:25:40] session which basically allowed me to
[1:25:43] reuse the same session for every
[1:25:44] download. It's going to be the same
[1:25:46] thing here with HTTPX and this async
[1:25:49] client. I'm going to reuse this same
[1:25:51] client for every download. Um, so we are
[1:25:55] using this. This is a context manager
[1:25:58] here. Um, so we are opening this up and
[1:26:00] we have this asynchronous compatible
[1:26:02] client here and then I'm creating my
[1:26:05] task group and then we are creating a
[1:26:08] bunch of these tasks. We're no longer
[1:26:09] sending it off to a thread. Now we're
[1:26:11] just passing this in directly and now
[1:26:14] we're also accepting this client here uh
[1:26:17] which I took out of the threading
[1:26:19] example. So let me go back up here and
[1:26:22] add this back in. So that was uh client
[1:26:27] and that is going to be an httpx
[1:26:31] uh async client there. And now instead
[1:26:37] of doing request.get
[1:26:40] uh that's going to be a client.get.
[1:26:42] Okay. So now we are making this download
[1:26:44] single image uh function here
[1:26:46] asynchronous. So we're going to make
[1:26:49] that asynchronous there. And let me
[1:26:52] clean this up a little bit here. Okay.
[1:26:56] And now again, I'm going to grab some
[1:26:58] code from my snippets file here. Now, I
[1:27:01] know I'm doing a lot of uh copying and
[1:27:04] pasting here. Uh but this tutorial is
[1:27:06] getting pretty long already uh as it is.
[1:27:09] So, I'm trying to use time here as
[1:27:12] effectively as I can. Um so this was uh
[1:27:17] I'm just copying and pasting some code
[1:27:19] here because uh while this is very
[1:27:21] similar to request it's not exactly like
[1:27:24] request. So uh I want to paste this in
[1:27:27] and I'll explain the differences here.
[1:27:28] Okay. So the first big difference before
[1:27:31] where we were doing this um response
[1:27:34] equals this was request.get here um now
[1:27:38] instead of that what we are doing is
[1:27:40] we're doing an await on this client.get
[1:27:43] get here uh with that async client. And
[1:27:47] also one other small change is that with
[1:27:49] the request library, it's allow
[1:27:51] redirects. This one is follow redirects.
[1:27:54] And also down here at the bottom, we
[1:27:56] were using uh normal file writes, but
[1:27:59] now we're using these AIO files to write
[1:28:03] asynchronously. And also I believe that
[1:28:05] this is the first time in this tutorial
[1:28:07] that we've seen an asynchronous iterator
[1:28:10] here uh where we're looping over these
[1:28:12] chunks from our response. Now normally
[1:28:15] with a regular iterator the four keyword
[1:28:18] here just pulls the next value
[1:28:21] immediately. But here every next chunk
[1:28:24] may involve waiting for IO like network
[1:28:27] data arriving. So Python gives us this
[1:28:30] async 4 to handle that gracefully. And
[1:28:34] this response aiter bytes here gives us
[1:28:37] an asynchronous iterator under the hood.
[1:28:41] Each loop step does an await to get the
[1:28:44] next chunk. Uh that's why you have to
[1:28:47] write async for chunk in blah blah blah
[1:28:50] blah blah. Um and inside that loop uh
[1:28:53] instead of using f.right
[1:28:57] uh we are awaiting that f.ight Right?
[1:29:00] Because now that chunk and this file
[1:29:03] here, everything here is asynchronous.
[1:29:06] Okay. So, let me go to where we're
[1:29:08] processing our images and I'll convert
[1:29:11] that to using processes instead of
[1:29:13] threads. So, down here is where we were
[1:29:16] sending this off to threads. Now, if you
[1:29:19] remember from our examples earlier,
[1:29:21] using processes is a little more
[1:29:22] complicated. Uh, let me grab that
[1:29:25] snippet here so we can see what this
[1:29:27] looks like. This is the last snippet uh
[1:29:30] that I have in my file here. So, it's
[1:29:31] the last one that I'm going to be
[1:29:33] grabbing. So, let me paste this in here.
[1:29:36] So, we saw this in one of our earlier
[1:29:38] examples, but we have to get the running
[1:29:40] loop using this async io.getrunning
[1:29:43] loop. Then, we are using this process
[1:29:45] pool executor here. We're creating a
[1:29:47] bunch of different tasks. We're doing
[1:29:49] loop.run run executor with this process
[1:29:52] pool executor passing in uh that process
[1:29:55] single image with the argument and we're
[1:29:58] doing that for all of the paths in our
[1:30:01] list of paths there to process and then
[1:30:04] we are awaiting those with async
[1:30:06] io.gather and passing in all those tasks
[1:30:09] there. Oh, and since I explicitly gave
[1:30:12] that uh hint earlier, I'm also going to
[1:30:15] do the return exceptions equal to true
[1:30:18] and follow my own advice there and do
[1:30:21] that. Okay, so now we're passing this
[1:30:23] image processing off to processes
[1:30:25] instead of threads and our download
[1:30:27] images are running asynchronous
[1:30:29] asynchronously on our current thread
[1:30:32] using async IO compatible libraries.
[1:30:35] Nothing else should need to change in
[1:30:37] our main function. So let's go ahead and
[1:30:40] run this and see what our speed up is
[1:30:43] now. This should go uh much much faster
[1:30:46] here. Okay, so that was obviously much
[1:30:48] faster. Uh you can see here that we uh
[1:30:52] ran this basically in 4.9 to 5 seconds
[1:30:55] in total. Uh compared to earlier, I
[1:30:58] wrote that down. Our total execution
[1:31:00] time was 23.54
[1:31:03] seconds. Um our downloads are now down
[1:31:06] to 1.6 6 seconds. Before that took 13
[1:31:10] seconds and our processing only took
[1:31:12] 3.25 seconds when before that took uh 10
[1:31:16] 12 seconds. So, a big speed up on
[1:31:18] everything there. Now, like I said, I
[1:31:20] know that this video is getting long,
[1:31:21] but one more thing before we end here. I
[1:31:24] want to show you some very quick things
[1:31:26] that we can add in here that you'll
[1:31:27] likely use at some point in your actual
[1:31:30] code. Now, it's nice that we've sped
[1:31:32] this up so drastically, but we're being
[1:31:35] a little greedy here in our script. So,
[1:31:37] right now, we're only grabbing 12 URLs,
[1:31:40] so it's not a huge deal to run all of
[1:31:42] these concurrently at once. Uh, but you
[1:31:45] might get into a situation where you're
[1:31:46] downloading thousands of URLs or trying
[1:31:49] to do thousands of things concurrently
[1:31:51] at the same time. If that's the case,
[1:31:54] then you might want to add some limits
[1:31:56] in place so that you're not kicking off
[1:31:58] all that stuff at the same time. It can
[1:32:00] bog down our own machine and it can also
[1:32:03] hammer servers. So, one easy way that we
[1:32:06] can do this is to use something called a
[1:32:09] semaphore. And for our processes, we can
[1:32:12] easily get our CPU count um of our
[1:32:14] current machine to limit the number of
[1:32:16] processes that can be spun up at a time.
[1:32:18] Uh so, let me add both these to our
[1:32:20] code. Uh, first let me go up here. Let
[1:32:24] me go up to the top here. I'm just going
[1:32:26] to import OS here so that we can get our
[1:32:31] CPU count. And now at the very top of
[1:32:33] our code, I'm going to add in a couple
[1:32:36] of of constants here. I'm just going to
[1:32:39] call one of these download limit. And
[1:32:41] I'll set this equal to four. So we can
[1:32:43] do four concurrent downloads at the same
[1:32:45] time. I'll create another one here
[1:32:47] called CPU workers. And I'll set this
[1:32:50] equal to OS. CPU count. Now to use a
[1:32:54] semaphore, I'm going to go update our
[1:32:56] download images function here. Um, right
[1:33:00] before we open up our async client, I'm
[1:33:04] just going to say that our download
[1:33:06] semaphore is equal to an async io.
[1:33:12] And we're just going to set that equal
[1:33:14] to that download limit. And our download
[1:33:18] limit was a constant of four. Oh, and
[1:33:20] just so I don't screw up later, let me
[1:33:23] fix that typo. That is a semaphore
[1:33:25] there. And now I'm going to pass this in
[1:33:28] as an argument to our download single
[1:33:31] image function here. So I'll just pass
[1:33:34] this in as the last argument there. It's
[1:33:36] giving me an error because we're not
[1:33:39] accepting that right now. Let me go up
[1:33:42] here and accept that. I'll just call
[1:33:45] this a simore. And then to type in that,
[1:33:49] I'll say that that is an async io.
[1:33:54] Okay, clean that up. And now to limit
[1:33:57] this to four downloads, I'm just going
[1:33:59] to take everything in this function here
[1:34:02] and I'm going to say async with that
[1:34:05] semaphore argument there. And I am just
[1:34:09] going to put everything here inside of
[1:34:13] that. And that's going to use that
[1:34:15] semaphore which will limit the maximum
[1:34:17] concurrent downloads to four at a time.
[1:34:19] Now for our processes, uh this one is
[1:34:22] actually the easiest part for once. Um
[1:34:25] so all I have to do is pass in our
[1:34:29] maximum number of workers here uh to
[1:34:32] this process pool executor. So I'll say
[1:34:35] max workers and I'll set that equal to
[1:34:37] the CPU workers constant that we set
[1:34:40] there at the top which was equal to the
[1:34:43] uh CPU count on our machine. So with
[1:34:46] those changes uh we're not being so
[1:34:48] greedy anymore. So even if we had
[1:34:50] thousands of images to download and
[1:34:52] process, we're still doing them quickly
[1:34:54] but uh not trying to spin up everything
[1:34:57] all at once. So now if I save this and
[1:35:00] run it. Oops, I have a uh mistake here.
[1:35:04] Let me go see what I spelled incorrectly
[1:35:06] here. Oh, you guys probably saw that I
[1:35:09] need an equal sign there instead of a
[1:35:12] minus sign. I hit a typo there. Okay, so
[1:35:15] now let me rerun this again. And now we
[1:35:19] can see whenever we first kicked that
[1:35:21] off, all four of these started at once
[1:35:24] and then it waited for one of these to
[1:35:26] come back before it started another one.
[1:35:29] So we have four concurrent downloads
[1:35:31] going on at a time. Um, but that is the
[1:35:35] max is we're uh not sending off more
[1:35:37] requests until one of those four are
[1:35:39] done. So overall, our speed is going to
[1:35:42] be slightly slower than before. Uh, but
[1:35:45] you're definitely going to want to use
[1:35:47] these limits uh like this once you get
[1:35:49] into doing a lot of work at once just to
[1:35:52] be easy on your machine and also to be
[1:35:54] easy on other servers. We still have a
[1:35:57] huge speed up here. um not as much as a
[1:36:00] speed up as just blasting everything off
[1:36:02] all at once, but we're using some smart
[1:36:05] limitations here and also getting a big
[1:36:08] speed up. So, it's the best of both
[1:36:10] worlds. Okay, so we're just about
[1:36:12] finished up here. Uh we covered a lot in
[1:36:14] this video, so let's start to wrap
[1:36:16] things up a bit. So, first I'd like to
[1:36:19] reiterate some common pitfalls that
[1:36:21] you'll run into when you first start
[1:36:23] working with Async IO. One mistake that
[1:36:26] many people make that you can see from
[1:36:29] time to time is forgetting to await
[1:36:31] their tasks or their co- routines. Um,
[1:36:34] just to show a very quick example of
[1:36:36] this, let me go back to one of these
[1:36:38] examples here. And so, for example,
[1:36:41] let's say that we don't await these
[1:36:44] tasks. Okay. Whoops. Let me delete that.
[1:36:48] Let me run this code here really quick.
[1:36:51] Now we can see that we got some print
[1:36:53] statements and stuff like that. Uh but
[1:36:55] we're not getting any actual errors. Uh
[1:36:58] but if you look closer then you'll
[1:37:00] notice that these tasks didn't actually
[1:37:02] run. You can see that they are printing
[1:37:05] out when we got the results to these
[1:37:06] that these tasks were canceled. Uh but
[1:37:08] if we didn't have a return statement
[1:37:10] here and I was to run this now we have a
[1:37:14] bunch of print statements with no errors
[1:37:16] at all and it looks like things were
[1:37:18] kind of successful. But if we look
[1:37:20] closer, we'll see that uh these tasks
[1:37:22] never actually ran and it just got to
[1:37:25] the start of those and they never
[1:37:26] finished. So, let me undo the changes
[1:37:29] that we made to that. Now, another issue
[1:37:32] that you might run into that's pretty
[1:37:34] common is that a script can end before
[1:37:36] tasks are complete, which you likely
[1:37:39] don't want. Um, so here where we are
[1:37:42] awaiting our task two, let me change
[1:37:45] this and instead of awaiting task two,
[1:37:47] I'll just await an async io. And I'll
[1:37:50] just do this for 0.1 seconds. Now that
[1:37:53] task two normally takes 2 seconds to
[1:37:56] complete. If I run this here, then we
[1:38:00] can see that we didn't get any errors
[1:38:02] here. Uh, but we got done with our first
[1:38:05] task, but we never got done with our
[1:38:08] second task. It says that task two was
[1:38:11] fully complete. That's just because I
[1:38:13] still have this print statement here.
[1:38:15] But what we were awaiting was this
[1:38:16] async.io.
[1:38:18] If we actually look at our results here,
[1:38:20] we can see that we have a result of one.
[1:38:22] Uh, but we do not have a result of two
[1:38:25] there. So you want to make sure that all
[1:38:27] of your tasks are uh awaited and that
[1:38:30] you don't have any still running uh
[1:38:33] before your script is completely done.
[1:38:35] Now of course another one of the most
[1:38:37] common mistakes is accidentally using
[1:38:39] blocking IO calls within async code. Uh
[1:38:42] we saw this several times throughout the
[1:38:44] video. Um so how do we avoid some of
[1:38:47] these issues? Well, one thing that helps
[1:38:49] is a good llinter. Uh my rough llinter
[1:38:52] settings point out a lot of async IO uh
[1:38:55] mistakes for me. If you haven't watched
[1:38:58] my video on rough uh and want to see how
[1:39:01] I have mine set up, then I'll leave a
[1:39:03] link to that video in the description
[1:39:04] section below. And also if you set uh
[1:39:08] debug equal to true here, debug is equal
[1:39:12] to true with this async io.run, run then
[1:39:16] that will help you uh find a lot of
[1:39:18] things that uh might be wrong in your
[1:39:20] async code as well. But you just want to
[1:39:23] make sure that you have that debug um
[1:39:25] set to true only whenever you are
[1:39:28] working in development. And lastly, just
[1:39:30] to reiterate when to use async IO uh
[1:39:33] versus threads versus processes. You'll
[1:39:36] usually want to use async IO when you
[1:39:39] have some IO bound work that you want to
[1:39:41] run concurrently. As long as there are
[1:39:44] asynchronous libraries uh to do that in
[1:39:47] async IO, then I would recommend doing
[1:39:49] it that way. If there aren't
[1:39:51] asynchronous libraries available and you
[1:39:54] have to use synchronous code for your
[1:39:56] IObound work, that's when you'd want to
[1:39:58] use threads. And multiprocessing is what
⚡ Saved you time reading this? Transcribe any YouTube video for free — no signup needed.