[0:00] Hey there. How's it going everybody? In [0:01] this video, we're going to be learning [0:03] all about Async.io in Python. Async.io [0:06] is Python's built-in library for writing [0:08] concurrent code. And it can seem a bit [0:10] intimidating at first with all the [0:12] different terminology and moving parts. [0:14] But by the end of this tutorial, I'm [0:16] hoping that you'll have a solid [0:17] understanding of how it works and when [0:19] to use it. Now, in this video, I'm going [0:21] to be covering a lot. We're going to be [0:23] learning how Async.io actually works [0:25] under the hood. We'll see visually [0:27] what's happening with some animations [0:28] that I've put together. Uh we'll see how [0:30] to update an existing codebase to use [0:32] async.io. We're going to be able to [0:35] determine when and where to use async.io [0:37] with some simple profiling. And we'll [0:39] discuss when to choose async versus [0:42] threads versus multiprocessing. And the [0:45] code in the animations that I'm going to [0:46] use in this video are going to be [0:48] available on my GitHub and website after [0:51] this is out. So, if you all want to [0:53] follow along with those yourselves, then [0:55] I'll leave links to those in the [0:56] description section below. Now, I'm [0:58] going to get to some code as soon as I [1:00] can, but there's a few basics that we [1:02] have to get out of the way first. So, [1:04] async.io is a Python library for writing [1:07] concurrent code using the async await [1:10] syntax. And I want to mention that we're [1:13] using the latest version of Python in [1:14] this video. And I'll be teaching the [1:17] current way to do things. Async.io has [1:19] evolved quite a bit over the years. Uh [1:21] there have been different ways of [1:23] running the event loop, scheduling and [1:25] running task and all kinds of different [1:27] changes. Uh but we're going to focus on [1:29] the modern ways that you should be using [1:31] today. Now before we dive too deep, [1:34] let's talk about what concurrency [1:36] actually is. So with synchronous code [1:39] execution, uh which is what we normally [1:41] write, one thing happens after another. [1:44] So, it's kind of like going to a Subway [1:47] restaurant where uh you put in your [1:49] order and they make your entire sandwich [1:51] from start to finish uh before moving on [1:54] to the next customer. But with [1:56] concurrent code, it's more like going to [1:58] a McDonald's where someone just takes [2:01] your order and then moves on to the next [2:04] customer while your food is being made [2:06] in the background. And that difference [2:08] when applied to code can be really [2:11] confusing at first for a lot of people. [2:13] So here's something that I think is [2:15] important to understand pretty early on. [2:17] So asynchronous doesn't automatically [2:20] mean faster. It just means that we can [2:22] do other useful work instead of sitting [2:26] idly by while waiting for things like [2:28] network requests and database queries [2:31] and stuff like that. That's why async IO [2:34] excels at what's called IObound tasks [2:37] which are anytime your program is [2:39] waiting for something external. Now, [2:41] Async.io is singlethreaded and runs on a [2:45] single process. It uses what's called [2:47] cooperative multitasking where tasks [2:50] voluntarily give up control. For [2:53] CPUbound tasks that need heavy [2:55] computation, you'd want to use processes [2:58] instead. And we'll see how to tell the [3:00] difference between IObound and CPUbound [3:03] here in a bit. But before I lose your [3:05] attention with too much theory early on, [3:07] let's go ahead and jump into some code [3:09] and we'll learn the basic terminology [3:11] that we need to know as we go. So, let [3:13] me walk through this code and explain [3:15] the terminology as we go. Now, the [3:17] terminology is what really trips a lot [3:19] of people up when they first start [3:20] learning async.io because there's quite [3:22] a bit to remember. Now, right now we [3:25] have some asynchronous code here that [3:27] we'll walk through and explain, but for [3:29] now you can see that we have a simple [3:31] synchronous function right here at the [3:33] top and that just sleeps for a bit and [3:36] then it returns a string. We're running [3:39] this synchronous function inside of our [3:42] main function here. Our main function is [3:46] asynchronous and you can see that it has [3:48] this async keyword. Since it's an [3:50] asynchronous function, we can't just [3:53] call it directly. Uh in order to run our [3:56] main function, we have to start what is [3:58] called an event loop. And we're doing [4:01] this down here at the bottom with async [4:04] io.run and passing in uh that main async [4:08] function there. So the event loop is [4:10] basically the engine that runs and [4:12] manages asynchronous functions. Think of [4:15] it a bit as auler. It keeps track of all [4:18] our tasks and when a task is suspended [4:21] because it's waiting for something else [4:24] uh control returns to the event loop [4:26] which then finds another task to either [4:29] start or resume. So we have to be [4:31] running an event loop for any of our [4:33] asynchronous code to work. That's what [4:35] this async io.run function is [4:37] responsible for. It's getting the event [4:40] loop running task until they're marked [4:42] as complete and then closing down the [4:44] event loop whenever it's done. So let me [4:47] go ahead and run the code that we have [4:49] right now. And we can see that we're [4:52] just printing out that this is a [4:53] synchronous function. And then we get [4:55] that result. So we came in here, ran [4:57] this event loop. This is an asynchronous [5:00] main function. And we are just calling [5:02] this synchronous code here within our [5:04] async function for now. But we don't [5:07] want to just run synchronous functions [5:09] inside of our event loop. We want to use [5:11] concurrency. to use concurrency. We're [5:14] going to be seeing this await keyword a [5:17] lot. So, let me uncomment the futures [5:20] code here uh so that we can talk about [5:24] awaitables. So, let me uncomment this [5:28] here. So, you can see here that we're [5:30] using this await keyword. So, awaitables [5:33] are objects that implement a special [5:35] await method under the hood. You're [5:38] going to see await everywhere in [5:40] asynchronous code. an object has to be [5:42] awaitable for us to use that keyword on [5:45] it. Now, why can't we await a [5:48] synchronous function uh like this sync [5:51] function here or something like [5:53] timesleep? Well, synchronous libraries [5:56] don't have a mechanism to work with the [5:58] event loop. They don't know how to yield [6:00] control over and resume later. So, [6:04] basically synchronous code like [6:06] time.sleep sleep or our synchronous [6:08] function here. Uh they don't have that [6:11] uh underlying special await function [6:13] that they need in order to pause their [6:15] execution and start back later. Uh these [6:18] things need to be coded in to be [6:20] compatible with async io. And that's why [6:23] we can't await time. And we need to use [6:26] something like async io.sleep instead. [6:30] And to use this await keyword, we also [6:32] have to be within a function that has [6:35] this async keyword. So if I remove async [6:40] from our function there, now you can see [6:42] that I'm getting a warning here. And if [6:45] I hover over this, uh, it's not letting [6:47] me hover right now, but we can see that [6:49] my rough warning here is telling me that [6:52] await should be used within an async [6:54] function. So we have to be within an [6:57] async function in order to use these [7:00] await keywords. Okay. So what does await [7:03] do? So when you await something, you're [7:06] basically telling the event loop to [7:08] pause the execution of the current [7:10] function and yield control back to the [7:13] event loop which can then run another [7:15] task and it'll stay suspended until this [7:19] awaitable completes. So in Python's [7:22] async io there are three main types of [7:24] awaitable objects. First there are co- [7:27] routines which are created when you call [7:29] an async function. Second there are [7:32] tasks and tasks are wrappers around co- [7:35] routines that are scheduled on the event [7:38] loop. And the third there are futures [7:40] and futures are low-level objects [7:43] representing eventual results. Now, if [7:46] you're coming from somewhere like the [7:48] JavaScript world, uh futures are a lot [7:51] like promises in JavaScript. They're a [7:54] promise of a result that will be [7:56] available later. But unlike JavaScript, [7:59] in Python, we almost never work with [8:01] futures directly. We write co- routines [8:03] and when we schedule them as tasks, [8:06] async.io uses futures under the hood to [8:09] track those results, but we won't be [8:11] seeing them much in this video. you're [8:13] really only going to use futures [8:15] directly if you were writing low-level [8:17] async IO code. Uh like if you were [8:20] building an Async compatible framework, [8:23] but just to show you what they look [8:24] like, let me run this example with uh [8:27] this futures example here really quick. [8:29] And uh just so we can see what's [8:32] happening. So a future's job is to hold [8:34] a certain state and result. The state [8:37] can be pending uh meaning the future [8:39] doesn't have any result or exception [8:42] yet. Uh it can be cancelled if it was [8:44] cancelled using future.canc [8:47] uh or it can be finished and it can be [8:50] finished uh by a result being set by uh [8:54] future set result or it can be an [8:57] exception with future set exception. So [9:00] you can see here after we created this [9:02] future and printed that out, it says [9:04] that future was pending after we created [9:06] it. And then we set the result right [9:09] here to future result test and got that [9:13] result by awaiting it. And then we [9:15] printed that out. But like I said, this [9:17] is lower level stuff and we won't be [9:20] using it directly in this video. We're [9:22] going to be working mostly with co- [9:24] routines and tasks. So I'm going to [9:26] delete this future example here and then [9:29] we're going to look at co-outines and [9:31] tasks. So let me uncomment the co-outine [9:35] example here. And co-outines are [9:37] functions defined with the async defaf [9:40] keywords here. So main is a co-outine [9:44] here. We have async defaf. If I go up [9:46] here, we have this async function and I [9:49] have a comment here that says also known [9:51] as a co-ine function. So we have this [9:53] async defaf async function. And within [9:56] here, it's a lot like our synchronous [9: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 [60:01] of stuff. Now you might be wondering [60:03] which ones you use here. Should you use [60:06] gather for a bunch of different tasks or [60:08] should you use a task group? Now, I tend [60:11] to use task groups a lot of the time, [60:14] but basically the key difference between [60:16] gather and task groups is how they [60:18] handle errors. So, with async io.gather, [60:22] if you use the default of return [60:26] exceptions is equal to false, which I [60:29] honestly wouldn't really recommend. And [60:32] as a matter of fact, I didn't realize [60:34] that I uh didn't have return exceptions [60:38] equal to true here in gather. Um so I [60:41] would always recommend if you're going [60:43] to use gather to use this return [60:45] exceptions equal to true. The default is [60:47] false. But with that set to false, if [60:51] one task fails, then it raises the first [60:54] exception that it saw. Uh you don't get [60:56] a bundle of errors or any of the [60:59] successful tasks. And if it fails, other [61:02] tasks won't be cancelled. So you risk [61:04] having orphaned task. Task group, it [61:08] also fails quickly, but it gives better [61:11] a uh better errors and handles cleanups [61:14] a bit better. So I wouldn't really use [61:16] gather with its default argument of [61:18] return exceptions equal to false. But it [61:21] does have a good use case for return [61:23] exceptions equal to true because if any [61:25] task does fail with return exceptions [61:28] equal to true, it still runs the other [61:31] task for you. Every awaitable in that [61:33] gather finishes whether it succeeds or [61:36] fails. So then your result is just a [61:39] list where each position is either the [61:42] result or uh of the success or the [61:45] exception of the failures. Um, this is [61:48] what you want to use if you want to run [61:51] all the tasks even if some error out. [61:54] Like maybe you have a bunch of URLs that [61:56] you want to crawl. Uh, but you don't [61:58] want all of them to fail if only one of [62:01] those URLs fails and gets hung up or [62:04] doesn't exist or something like that. [62:06] For that, you would use async.io. [62:09] Now, with async.io.task task group here [62:12] it also on the first failure uh it [62:15] cancels all the other tasks. So if it [62:18] fails it raises an exception group [62:21] containing all exceptions from the [62:23] failed tasks and that would include [62:25] exceptions from canceled tasks as well. [62:28] Now there's no option to keep running [62:30] other tasks after one fails. So we use [62:34] this when we want all our tasks to run [62:36] successfully. So basically to sum all [62:39] that up, if you want tasks to continue [62:42] running even if some fail, then you [62:45] would use gather with return exceptions [62:48] equal to true. Now, but if you want all [62:51] of the tasks to either fail together or [62:53] succeed together, then you would use a [62:56] task group. And I almost never would [63:00] recommend using uh gather with the [63:02] default of return exceptions equal to [63:05] false. If somebody knows of some edge [63:07] cases that I'm not thinking of, then [63:08] feel free to uh leave me a comment. But [63:12] um you know, if that's the case, I would [63:14] uh if you want it to fail fast, then I [63:16] almost always use a task group instead. [63:19] Okay, so just like with the other [63:21] examples, let's look at this example in [63:23] the browser here to see what's going on [63:25] under the hood. Uh but I'm going to run [63:27] through this animation a little faster [63:30] because it's going to be very similar to [63:32] the examples that we've already seen. Uh [63:34] the tasks are going to be scheduled and [63:37] awaited in different ways than we've [63:39] seen before, but the behaviors are [63:41] basically the same. Um so let's step [63:44] through this pretty quickly here. We're [63:46] creating a task. It gets scheduled. [63:48] Creating another task that gets [63:50] scheduled. We are awaiting our main co [63:53] routine here and suspending it. Running [63:55] our first task that suspends. Our second [63:59] task suspends after we kick off both of [64:02] these background IO tasks here. And [64:05] actually, so I'm not scrolling here, let [64:08] me make the screen a little bit smaller. [64:10] I know that's going to be harder to [64:11] read. Uh, but basically, we just want to [64:14] see these animations anyway. Um, the [64:16] code is the same as it was in our [64:18] example here. Um, but [64:22] this is the examples that we've seen [64:24] already where we're creating these [64:26] manually. That's why I'm stepping [64:27] through these pretty quickly. Okay, so [64:29] now we are getting to our gather [64:32] examples. So first we're creating this [64:34] list of co-outines here and that's not [64:38] going to schedule anything on our event [64:40] loop and now we are here where we are [64:44] gathering all of those and we are [64:47] awaiting that gather. Now since these [64:49] are co-ines these are going to get [64:51] scheduled and run to completion at the [64:54] same time. So that is going to suspend [64:56] our main co-ine and schedule all of [65:00] those uh co-ines that were in this list. [65:04] So now just like we saw before, whoops, [65:07] let me get that there. Uh just like [65:10] before, it's going to run through since [65:12] we have both of these scheduled on our [65:14] event loop. It's going to run these [65:16] concurrently until these complete and [65:20] return and complete and return. And once [65:24] all of those are done, then it's going [65:26] to uh tell our main co- routine here [65:29] that it's ready and it's going to go [65:31] ahead and move forward. Now with our [65:34] gather task, this is a little bit uh [65:37] different because we are creating tasks [65:39] here in a list instead of creating co- [65:42] routines in a list. Now remember when we [65:44] create a task, it's going to go ahead [65:46] and schedule those. So those get [65:49] scheduled when we create those tasks and [65:52] then when we await those uh that is [65:56] whenever we are going to suspend our [65:59] main co-outine and our event loop can [66:01] actually get around to running those. [66:04] But those are going to run concurrently [66:06] like we've seen before. [66:09] And once those are done, then our await [66:12] is going to go ahead and wake up our [66:15] main co-outine here and go ahead and [66:18] move forward. And then with our task [66:21] group, we're going to come in here to [66:23] our task group. We are going to create [66:26] these tasks in our task group. So those [66:28] are going to get scheduled onto our [66:30] event loop. Now there is no await [66:32] statement here. This automatically [66:35] awaits whenever it exits our context [66:39] manager here. So once that context [66:42] manager ends, that's when we suspend our [66:45] co- routine there and then our event [66:47] loop can find these ready tasks and run [66:50] these. [66:52] So just like before, these are run [66:55] concurrently. [66:57] And once those are done, that's whenever [66:59] it can fully exit that um task group [67:03] context manager there and move on to our [67:06] print statement that it has those task [67:09] group results. So then we are finished [67:11] up with our main co-outine here that [67:14] completes down here at the bottom. We [67:17] are free to move on and print out our [67:20] results. Okay, so that is the last [67:22] animation and that is the last of our [67:24] code examples. Now we can look at a [67:27] realworld example here. I hope that [67:29] those animations really helped visualize [67:31] what's going on in the background and [67:33] helped make it easier to understand how [67:35] Async IO works. Um, with my brain, I can [67:38] kind of understand stuff better when [67:40] it's visual like that and interactive [67:42] like that. Um, but now let's look at a [67:45] real world example and we'll see how to [67:47] convert this real world example of some [67:49] synchronous code and convert this over [67:52] to using async IO. So, let me open up [67:55] this real world example here. And first, [67:58] we'll just uh look over what this does. [68:01] So, I have a synchronous script here [68:02] that downloads some images and then it [68:04] processes those images. So, let me walk [68:07] through this and explain what's going [68:09] on. So, I have my imports up here. I [68:12] have my image URLs. This is just 12 uh [68:15] highdefinition pictures here. And then I [68:18] have a download single image function, [68:21] download images function that loops over [68:24] uh those URLs and downloads the single [68:26] images for each of those URLs. We have a [68:29] process single image here. And I just [68:32] grabbed this offline. This is uh an [68:34] algorithm that does edge detection on [68:37] photos. So it's just some photo [68:39] processing there. Um, we have a process [68:42] images here that loops over all of the [68:45] images that we need to process that we [68:47] downloaded and processes one at a time. [68:50] And then we have our main method here. [68:53] And in our main method, uh, just ignore [68:56] I have a bunch of timing uh, stuff here [68:59] just to see how long this script takes [69:01] to run. But the big part is that we have [69:03] our image paths where we are downloading [69:05] all of our images. And then we have our [69:07] process paths where we are processing [69:10] all of those images that we downloaded. [69:12] And then I have some final wrapping up [69:15] here with the timing. And then I'm [69:17] printing out um all the images that we [69:19] download and how long everything took. [69:22] And then finally down here at the bottom [69:24] I am running that main function. So, let [69:27] me go ahead and run this and we'll see [69:30] how long this synchronous script takes. [69:33] So, we can see that we are downloading [69:35] some images here and I might fast [69:38] forward a little bit until this is done. [69:41] Okay, so I fast forwarded a bit there uh [69:43] just so we didn't have to watch all of [69:45] these images download and process. But [69:47] at the bottom, we can see how long this [69:50] took. Um, I'm actually going to copy [69:53] this uh so that we can uh reference this [69:56] later. I'm going to paste it into some [69:58] of my notes here off screen. But we can [70:00] see that the total execution time here [70:02] was about 23 seconds. Um, the time that [70:06] it took to download the images was 13 [70:09] seconds, which was 5.5% of the total [70:12] time when we processed our images in [70:15] about 10 to 11 seconds, which was about [70:17] 44% of the time. So, we're going to be [70:19] able to speed this up by a lot. Uh, but [70:22] I don't know about you, but when I was [70:24] first learning async.io, I didn't even [70:26] really know where to start with, uh, you [70:29] know, in terms of where to make changes [70:30] to the synchronous script. Uh, which [70:33] parts do I make asynchronous? Do I use [70:36] threads or do I use processes? How do I [70:38] know? Well, here are some tips that I [70:40] can give uh to give you an idea of where [70:43] to start. So first we need to determine [70:46] what's IObound and what's CPUbound. Like [70:50] I said before, IObound work is where [70:52] we're just waiting around on external [70:54] things to be done like web request, [70:57] database access, uh file access, things [71:00] like that. It's often easy to guess [71:03] what's IObound in your code. Uh if you [71:06] know what you're looking for, you can [71:07] even look for certain words. You know, [71:09] words like fetch or get or web request [71:13] or database. Um those usually lean [71:16] towards IObound type of things. Uh [71:19] certain words like compute or calculate [71:22] or process, those usually lean towards [71:25] CPUbound type of tasks. Um now once you [71:29] work with this stuff more and more, [71:30] that's going to become more intuitive. [71:33] But if you want to be absolutely sure, [71:36] we can use actual profiling tools to see [71:39] where we're spending the most of our [71:40] time in our code. So I'm going to show a [71:43] profiling example using scaline, which [71:46] is a really nice profiler that I like. [71:48] And we're not even going to need to add [71:50] anything extra to our code. So first I'm [71:54] going to install uh scaling here. And [71:57] you can install it with pip or UV. I'm [72:00] going to use UV here. Uh, so you could [72:02] do a pip install scaline. I'm going to [72:05] use uv and do a uvad scaline. I think I [72:09] spelled that right. And once that's [72:11] installed, I'm going to run a command [72:13] that will profile our code. Now, I have [72:16] some snippets uh open here that I'm [72:19] going to use to copy this command in uh [72:22] just so I don't mistype anything here. [72:25] So, let me copy this and then paste this [72:29] in. Now since I'm using UV here, I'm [72:32] doing a UV run-m you can all you also [72:35] use a python-m [72:37] there. Um, so I'm running this scaline [72:40] module. I'm creating an HTML report here [72:44] called profile report and we are going [72:47] to profile uh my script here is called [72:50] real world example sync v1. So, I'm [72:53] going to run that, and it's going to [72:55] rerun our script where it's going to [72:57] download uh those images and process [73:00] those images again, but it's going to [73:02] profile it as it goes and give us an [73:04] idea of where we're spending most of [73:06] that time. And once this is done, I will [73:09] go ahead and open this profile [73:11] report.html in my browser. Okay, so that [73:15] script finished. Uh it actually took a [73:17] good bit longer that time. That time it [73:19] took 29 seconds. Uh the last time it [73:21] only took 23 of the 24 seconds. Um but [73:25] let me go ahead and open this profile [73:29] report that it gave us here in the [73:32] browser. I will allow that. Okay. So [73:35] here's our report. Let me make this uh a [73:38] lot larger here so that we can uh so [73:41] that you can read this. Okay. So we can [73:44] see here that it breaks up the time here [73:47] between Python time, native time and [73:50] system time. Now the system time is [73:53] likely a lot of waiting on IO here. And [73:57] we can see that most of our system time [74:01] is here in this download single image [74:05] function here. So that is a great [74:07] indicator that we can speed up uh this [74:10] download single image with async io or [74:13] threads if you want to use threads also. [74:16] Now a lot of the time being spent in [74:18] python is here within our process single [74:22] image function. So that likely means [74:25] that that is CPUbound. Um so we'd get [74:28] more of a speed up using multiple [74:30] processes on that than we would from [74:32] async IO or threads. So now that we've [74:36] profiled this, we have some actionable [74:38] knowledge of where we can speed up our [74:40] code. So let's go change these specific [74:43] functions. So I'm going to go back to [74:45] our Python code here. I'm going to close [74:47] down our output there. And now let's go [74:50] to the top and let's import async IO. [74:55] Um, now for this first example, let's [74:59] assume that I don't know about any [75:02] asynchronous libraries that we can use [75:05] to do these web requests. So, there are [75:07] asynchronous libraries out there that [75:09] make this easier like HTTPX or AIO HTTP. [75:14] But let's say that for now I just wanted [75:17] to keep using uh requests. We can see [75:19] here that I'm using um a request session [75:23] here and I'm using a session.get with [75:26] that request library. Now, like I've [75:28] been saying, request isn't asynchronous. [75:31] So, to continue using requests, we [75:33] really don't have any choice other than [75:35] to use threads. Uh we saw how to do this [75:38] earlier. So, let's go ahead and change [75:40] this to use threads and have Async.io [75:44] manage those threads. So, I'm going to [75:46] go up to our download single image here. [75:48] Now, the first thing I'm going to do, I [75:50] don't know if these sessions are thread [75:52] safe, so I'm going to remove this [75:56] session as an argument here, and I'm [75:59] just going to use request.git instead of [76:03] session.git there. Um, that'll have to [76:07] create a new session every time for each [76:10] of those URLs. Uh, but that's okay. But [76:12] other than that one small little change, [76:15] since we're going to use threads for [76:16] now, I'm not going to touch this [76:18] function anymore. I'm going to leave [76:20] this as a regular synchronous function. [76:23] Um, I'm going to go to where it's [76:24] calling this function to make the [76:26] changes that we need. And it's calling [76:28] this function in our download images [76:31] function here. So within download [76:33] images, this is where we'll use async.io [76:36] to send this off to threads. So that [76:38] means that we're going to convert this [76:40] to a co-outine. And to do that, the [76:42] first thing that we need to do is add [76:44] async before the function here. And I'm [76:48] no longer using uh sessions since I [76:50] wasn't sure if that was thread safe. So [76:53] I'm going to take out that context [76:55] manager there and unindent those. And [76:59] I'm no longer passing in that session as [77:02] an argument there. Okay. So now we're [77:04] running this single download single [77:06] image function for every URL in our URLs [77:09] list. So instead, let's send that off to [77:13] a thread. To do this, we can use gather [77:16] or we can use a task group. I'm going to [77:18] use a task group. And I'm actually going [77:20] to grab a little bit of code from my [77:22] snippets file here. And I'll paste this [77:24] in above uh what we have now so that we [77:27] can see the difference. I just don't [77:29] want to accidentally uh mistype anything [77:32] and have to do a bunch of debugging in [77:35] the middle of a tutorial. So let me just [77:39] copy this from my snippets here. And so [77:43] this is what we had before. And this is [77:46] what we have now with this task group. [77:49] So you can see here that we are creating [77:52] our task group here. And within here [77:54] we're creating a bunch of tasks. And [77:56] we're saying task groupoup.create task. [77:58] And now instead of running this download [78:02] single image directly, we're now sending [78:05] this off to an uh using async io.2 [78:09] thread. And this create task method here [78:12] is going to create tasks uh for us of [78:15] all those threads uh that we can await. [78:18] Now like we saw before, this task group [78:20] is going to await on its own after it [78:23] exits this context manager here. So [78:26] outside of this context manager uh to [78:28] get the results from all of those tasks, [78:32] I can just create a new list here and [78:34] say task.result for task in task. So now [78:37] that we have that, I'm going to go ahead [78:39] and just remove what we had before and [78:42] use this new way of doing it here and [78:44] save that. And now just to show that [78:46] it's important to know when to use [78:48] async.io versus threads versus processes [78:51] correctly. Let me also shove our image [78:55] processing off into threads. Also, uh [78:58] judging from our profiling that we saw [79:00] earlier, our processing should be [79:02] CPUbound. So, we shouldn't get any speed [79:05] up by uh shoving these onto threads. And [79:09] I want to be able to show that. So, let [79:11] me go ahead and put those in threads [79:13] just so we can show uh that it we don't [79:16] get a speed up there. Um, so I'm going [79:18] to go down to our process images [79:20] function here. And I'm also going to [79:23] make this a co- routine as well. And [79:26] just like we did for our downloads, let [79:28] me go ahead and grab a small snippet [79:30] here. This is going to be very similar [79:33] uh to our downloads code here. Again, [79:36] I'll put it right above what we had [79:39] before, but we are basically doing the [79:42] same thing here. We're creating a task [79:43] group. within that task group, we're [79:45] creating a bunch of tasks. And what [79:48] those tasks are uh is we are uh passing [79:52] this process single image here off to a [79:55] thread and it's going to do that for [79:58] every image that we have in our list of [80:01] images. And then once we exit that [80:03] context manager, uh we are just getting [80:07] a another list comprehension here uh [80:09] setting the task. for every task in that [80:12] task list. Okay. So now I'm going to uh [80:16] remove the old way of doing that there. [80:19] Now I can't run this quite yet. Uh since [80:22] we converted these to co- routines, we [80:24] now need to await these and also run an [80:27] event loop. Uh so we're calling these [80:30] process images and this uh download [80:34] images here. We are calling those within [80:36] our main function here. Uh so within our [80:40] main function, I'm also going to convert [80:43] this to an async function because I [80:45] can't await those uh if I'm not in an [80:48] async function. And now that we are in [80:52] uh an async function here, instead of [80:54] setting our image paths equal to [80:56] download images, I'm instead going to [80:59] await download images since that's now a [81:02] co-outine. And I'm also going to await [81:05] process images as well. And lastly uh [81:09] where we are running our main function [81:11] down here. This is now our main function [81:14] is now the main entry point for our [81:16] asynchronous code. So we need to run [81:18] that in an event loop. So to do that I'm [81:21] going to say async io.run [81:25] and I'm going to pass that in to run. [81:28] Okay. So hopefully I typed everything [81:31] correctly there. Um, now let me rerun [81:34] this and let's see uh if we're able to [81:36] get a speed up by passing all of those [81:39] off to threads. So we can see that it [81:41] kicked off a lot of downloads uh really [81:44] quickly and downloaded those. Uh but the [81:47] processed images, it's still taking a [81:50] while here. And you can see with threads [81:53] some of this uh output gets buffered a [81:55] little weird and puts them on the same [81:57] lines. But that's okay. We're not too [81:59] concerned about uh this uh weird output [82:01] up here. That's just showing us what [82:03] those threads were printing out with the [82:05] process images and stuff. This is mainly [82:07] what we're concerned with here is how [82:10] long it took. So, right off the bat, you [82:13] can see that it sped up these downloads [82:15] by a lot. Uh they all ran in their own [82:18] threads concurrently. Um, before I wrote [82:21] this down here, before our download of [82:23] 12 images took 13 seconds and now it's [82:27] taking 2 seconds. And our process images [82:30] last time took 10.49 seconds. Now it [82:34] took 10.63. [82:36] So it sped up our downloads by a lot, [82:39] but it didn't speed up our processing, [82:41] which we expected. Um, so that's good. [82:44] So it's showing us that we sped up our [82:46] IObound code but not our CPUbound code. [82:49] Okay. So now let's take another look at [82:52] how we can improve this code. So right [82:54] now we've just shoved everything off to [82:56] threads. But you really only want to do [82:59] that when there's no asynchronous [83:01] alternative. But for rew uh requests we [83:04] do have async.io compatible libraries. [83:06] Uh the two most popular are HTTPX and [83:10] AIO HTTP. I'm going to use HTTPX in this [83:15] example uh because it's more similar to [83:17] requests. And we've also got some file [83:20] reading and writing that we can pass off [83:22] to an asynchronous library as well. And [83:25] for that we can use a io files package. [83:29] So let me go ahead and install these. [83:31] I'll open up our terminal again here. Um [83:35] now just like before you can use pip [83:37] install or I'm using uv so I'm going to [83:40] add these with uv. So, I'm going to use [83:43] httpx [83:44] for the web requests and I'm going to [83:48] use um a io files for the uh file IO. [83:55] So, now that we have those installed, uh [83:57] let's go up here and let's update our [84:00] imports. So, I'm going to uh import [84:05] httpx. [84:07] I'm also going to import a io files and [84:13] I'm no longer going to use requests. So [84:15] I'm going to get rid of requests there. [84:18] And also for our image processing, we [84:20] saw that the threads didn't speed that [84:22] up, which we knew was going to happen. [84:24] Um, but I wanted to show how someone [84:26] might try that anyway. Uh, but instead, [84:30] uh, let's pass those off to processes. [84:32] So adding additional processes for that [84:34] work is how we speed up CPUbound tasks. [84:37] So to do this I'm going to import the [84:40] process pool executor from uh [84:42] concurrent.futures. [84:44] So that's from concurrent. [84:46] Futures and we are going to import the [84:50] process pool executor. Okay. It [84:53] automatically u sorts my imports there. [84:56] So if those jump around, don't worry [84:58] about that. Okay. So now let's turn our [85:00] download single image uh function here [85:04] into an asynchronous function that uses [85:06] httpx. [85:08] Uh but first I'm going to update the [85:10] download images function here first. Um [85:13] and again I'm going to grab this from my [85:16] snippets file and I'll explain what we [85:19] changed. Uh just so I don't mistype [85:21] anything here. Let me grab this from my [85:25] snippets and paste this in. and then [85:28] I'll explain what we changed here. Now, [85:32] if you remember before I passed this off [85:34] to threads with the request library, I [85:37] was doing something like this with [85:38] requests. I was do using a request [85:40] session which basically allowed me to [85:43] reuse the same session for every [85:44] download. It's going to be the same [85:46] thing here with HTTPX and this async [85:49] client. I'm going to reuse this same [85:51] client for every download. Um, so we are [85:55] using this. This is a context manager [85:58] here. Um, so we are opening this up and [86:00] we have this asynchronous compatible [86:02] client here and then I'm creating my [86:05] task group and then we are creating a [86:08] bunch of these tasks. We're no longer [86:09] sending it off to a thread. Now we're [86:11] just passing this in directly and now [86:14] we're also accepting this client here uh [86:17] which I took out of the threading [86:19] example. So let me go back up here and [86:22] add this back in. So that was uh client [86:27] and that is going to be an httpx [86:31] uh async client there. And now instead [86:37] of doing request.get [86:40] uh that's going to be a client.get. [86:42] Okay. So now we are making this download [86:44] single image uh function here [86:46] asynchronous. So we're going to make [86:49] that asynchronous there. And let me [86:52] clean this up a little bit here. Okay. [86:56] And now again, I'm going to grab some [86:58] code from my snippets file here. Now, I [87:01] know I'm doing a lot of uh copying and [87:04] pasting here. Uh but this tutorial is [87:06] getting pretty long already uh as it is. [87:09] So, I'm trying to use time here as [87:12] effectively as I can. Um so this was uh [87:17] I'm just copying and pasting some code [87:19] here because uh while this is very [87:21] similar to request it's not exactly like [87:24] request. So uh I want to paste this in [87:27] and I'll explain the differences here. [87:28] Okay. So the first big difference before [87:31] where we were doing this um response [87:34] equals this was request.get here um now [87:38] instead of that what we are doing is [87:40] we're doing an await on this client.get [87:43] get here uh with that async client. And [87:47] also one other small change is that with [87:49] the request library, it's allow [87:51] redirects. This one is follow redirects. [87:54] And also down here at the bottom, we [87:56] were using uh normal file writes, but [87:59] now we're using these AIO files to write [88:03] asynchronously. And also I believe that [88:05] this is the first time in this tutorial [88:07] that we've seen an asynchronous iterator [88:10] here uh where we're looping over these [88:12] chunks from our response. Now normally [88:15] with a regular iterator the four keyword [88:18] here just pulls the next value [88:21] immediately. But here every next chunk [88:24] may involve waiting for IO like network [88:27] data arriving. So Python gives us this [88:30] async 4 to handle that gracefully. And [88:34] this response aiter bytes here gives us [88:37] an asynchronous iterator under the hood. [88:41] Each loop step does an await to get the [88:44] next chunk. Uh that's why you have to [88:47] write async for chunk in blah blah blah [88:50] blah blah. Um and inside that loop uh [88:53] instead of using f.right [88:57] uh we are awaiting that f.ight Right? [89:00] Because now that chunk and this file [89:03] here, everything here is asynchronous. [89:06] Okay. So, let me go to where we're [89:08] processing our images and I'll convert [89:11] that to using processes instead of [89:13] threads. So, down here is where we were [89:16] sending this off to threads. Now, if you [89:19] remember from our examples earlier, [89:21] using processes is a little more [89:22] complicated. Uh, let me grab that [89:25] snippet here so we can see what this [89:27] looks like. This is the last snippet uh [89:30] that I have in my file here. So, it's [89:31] the last one that I'm going to be [89:33] grabbing. So, let me paste this in here. [89:36] So, we saw this in one of our earlier [89:38] examples, but we have to get the running [89:40] loop using this async io.getrunning [89:43] loop. Then, we are using this process [89:45] pool executor here. We're creating a [89:47] bunch of different tasks. We're doing [89:49] loop.run run executor with this process [89:52] pool executor passing in uh that process [89:55] single image with the argument and we're [89:58] doing that for all of the paths in our [90:01] list of paths there to process and then [90:04] we are awaiting those with async [90:06] io.gather and passing in all those tasks [90:09] there. Oh, and since I explicitly gave [90:12] that uh hint earlier, I'm also going to [90:15] do the return exceptions equal to true [90:18] and follow my own advice there and do [90:21] that. Okay, so now we're passing this [90:23] image processing off to processes [90:25] instead of threads and our download [90:27] images are running asynchronous [90:29] asynchronously on our current thread [90:32] using async IO compatible libraries. [90:35] Nothing else should need to change in [90:37] our main function. So let's go ahead and [90:40] run this and see what our speed up is [90:43] now. This should go uh much much faster [90:46] here. Okay, so that was obviously much [90:48] faster. Uh you can see here that we uh [90:52] ran this basically in 4.9 to 5 seconds [90:55] in total. Uh compared to earlier, I [90:58] wrote that down. Our total execution [91:00] time was 23.54 [91:03] seconds. Um our downloads are now down [91:06] to 1.6 6 seconds. Before that took 13 [91:10] seconds and our processing only took [91:12] 3.25 seconds when before that took uh 10 [91:16] 12 seconds. So, a big speed up on [91:18] everything there. Now, like I said, I [91:20] know that this video is getting long, [91:21] but one more thing before we end here. I [91:24] want to show you some very quick things [91:26] that we can add in here that you'll [91:27] likely use at some point in your actual [91:30] code. Now, it's nice that we've sped [91:32] this up so drastically, but we're being [91:35] a little greedy here in our script. So, [91:37] right now, we're only grabbing 12 URLs, [91:40] so it's not a huge deal to run all of [91:42] these concurrently at once. Uh, but you [91:45] might get into a situation where you're [91:46] downloading thousands of URLs or trying [91:49] to do thousands of things concurrently [91:51] at the same time. If that's the case, [91:54] then you might want to add some limits [91:56] in place so that you're not kicking off [91:58] all that stuff at the same time. It can [92:00] bog down our own machine and it can also [92:03] hammer servers. So, one easy way that we [92:06] can do this is to use something called a [92:09] semaphore. And for our processes, we can [92:12] easily get our CPU count um of our [92:14] current machine to limit the number of [92:16] processes that can be spun up at a time. [92:18] Uh so, let me add both these to our [92:20] code. Uh, first let me go up here. Let [92:24] me go up to the top here. I'm just going [92:26] to import OS here so that we can get our [92:31] CPU count. And now at the very top of [92:33] our code, I'm going to add in a couple [92:36] of of constants here. I'm just going to [92:39] call one of these download limit. And [92:41] I'll set this equal to four. So we can [92:43] do four concurrent downloads at the same [92:45] time. I'll create another one here [92:47] called CPU workers. And I'll set this [92:50] equal to OS. CPU count. Now to use a [92:54] semaphore, I'm going to go update our [92:56] download images function here. Um, right [93:00] before we open up our async client, I'm [93:04] just going to say that our download [93:06] semaphore is equal to an async io. [93:12] And we're just going to set that equal [93:14] to that download limit. And our download [93:18] limit was a constant of four. Oh, and [93:20] just so I don't screw up later, let me [93:23] fix that typo. That is a semaphore [93:25] there. And now I'm going to pass this in [93:28] as an argument to our download single [93:31] image function here. So I'll just pass [93:34] this in as the last argument there. It's [93:36] giving me an error because we're not [93:39] accepting that right now. Let me go up [93:42] here and accept that. I'll just call [93:45] this a simore. And then to type in that, [93:49] I'll say that that is an async io. [93:54] Okay, clean that up. And now to limit [93:57] this to four downloads, I'm just going [93:59] to take everything in this function here [94:02] and I'm going to say async with that [94:05] semaphore argument there. And I am just [94:09] going to put everything here inside of [94:13] that. And that's going to use that [94:15] semaphore which will limit the maximum [94:17] concurrent downloads to four at a time. [94:19] Now for our processes, uh this one is [94:22] actually the easiest part for once. Um [94:25] so all I have to do is pass in our [94:29] maximum number of workers here uh to [94:32] this process pool executor. So I'll say [94:35] max workers and I'll set that equal to [94:37] the CPU workers constant that we set [94:40] there at the top which was equal to the [94:43] uh CPU count on our machine. So with [94:46] those changes uh we're not being so [94:48] greedy anymore. So even if we had [94:50] thousands of images to download and [94:52] process, we're still doing them quickly [94:54] but uh not trying to spin up everything [94:57] all at once. So now if I save this and [95:00] run it. Oops, I have a uh mistake here. [95:04] Let me go see what I spelled incorrectly [95:06] here. Oh, you guys probably saw that I [95:09] need an equal sign there instead of a [95:12] minus sign. I hit a typo there. Okay, so [95:15] now let me rerun this again. And now we [95:19] can see whenever we first kicked that [95:21] off, all four of these started at once [95:24] and then it waited for one of these to [95:26] come back before it started another one. [95:29] So we have four concurrent downloads [95:31] going on at a time. Um, but that is the [95:35] max is we're uh not sending off more [95:37] requests until one of those four are [95:39] done. So overall, our speed is going to [95:42] be slightly slower than before. Uh, but [95:45] you're definitely going to want to use [95:47] these limits uh like this once you get [95:49] into doing a lot of work at once just to [95:52] be easy on your machine and also to be [95:54] easy on other servers. We still have a [95:57] huge speed up here. um not as much as a [96:00] speed up as just blasting everything off [96:02] all at once, but we're using some smart [96:05] limitations here and also getting a big [96:08] speed up. So, it's the best of both [96:10] worlds. Okay, so we're just about [96:12] finished up here. Uh we covered a lot in [96:14] this video, so let's start to wrap [96:16] things up a bit. So, first I'd like to [96:19] reiterate some common pitfalls that [96:21] you'll run into when you first start [96:23] working with Async IO. One mistake that [96:26] many people make that you can see from [96:29] time to time is forgetting to await [96:31] their tasks or their co- routines. Um, [96:34] just to show a very quick example of [96:36] this, let me go back to one of these [96:38] examples here. And so, for example, [96:41] let's say that we don't await these [96:44] tasks. Okay. Whoops. Let me delete that. [96:48] Let me run this code here really quick. [96:51] Now we can see that we got some print [96:53] statements and stuff like that. Uh but [96:55] we're not getting any actual errors. Uh [96:58] but if you look closer then you'll [97:00] notice that these tasks didn't actually [97:02] run. You can see that they are printing [97:05] out when we got the results to these [97:06] that these tasks were canceled. Uh but [97:08] if we didn't have a return statement [97:10] here and I was to run this now we have a [97:14] bunch of print statements with no errors [97:16] at all and it looks like things were [97:18] kind of successful. But if we look [97:20] closer, we'll see that uh these tasks [97:22] never actually ran and it just got to [97:25] the start of those and they never [97:26] finished. So, let me undo the changes [97:29] that we made to that. Now, another issue [97:32] that you might run into that's pretty [97:34] common is that a script can end before [97:36] tasks are complete, which you likely [97:39] don't want. Um, so here where we are [97:42] awaiting our task two, let me change [97:45] this and instead of awaiting task two, [97:47] I'll just await an async io. And I'll [97:50] just do this for 0.1 seconds. Now that [97:53] task two normally takes 2 seconds to [97:56] complete. If I run this here, then we [98:00] can see that we didn't get any errors [98:02] here. Uh, but we got done with our first [98:05] task, but we never got done with our [98:08] second task. It says that task two was [98:11] fully complete. That's just because I [98:13] still have this print statement here. [98:15] But what we were awaiting was this [98:16] async.io. [98:18] If we actually look at our results here, [98:20] we can see that we have a result of one. [98:22] Uh, but we do not have a result of two [98:25] there. So you want to make sure that all [98:27] of your tasks are uh awaited and that [98:30] you don't have any still running uh [98:33] before your script is completely done. [98:35] Now of course another one of the most [98:37] common mistakes is accidentally using [98:39] blocking IO calls within async code. Uh [98:42] we saw this several times throughout the [98:44] video. Um so how do we avoid some of [98:47] these issues? Well, one thing that helps [98:49] is a good llinter. Uh my rough llinter [98:52] settings point out a lot of async IO uh [98:55] mistakes for me. If you haven't watched [98:58] my video on rough uh and want to see how [99:01] I have mine set up, then I'll leave a [99:03] link to that video in the description [99:04] section below. And also if you set uh [99:08] debug equal to true here, debug is equal [99:12] to true with this async io.run, run then [99:16] that will help you uh find a lot of [99:18] things that uh might be wrong in your [99:20] async code as well. But you just want to [99:23] make sure that you have that debug um [99:25] set to true only whenever you are [99:28] working in development. And lastly, just [99:30] to reiterate when to use async IO uh [99:33] versus threads versus processes. You'll [99:36] usually want to use async IO when you [99:39] have some IO bound work that you want to [99:41] run concurrently. As long as there are [99:44] asynchronous libraries uh to do that in [99:47] async IO, then I would recommend doing [99:49] it that way. If there aren't [99:51] asynchronous libraries available and you [99:54] have to use synchronous code for your [99:56] IObound work, that's when you'd want to [99:58] use threads. And multiprocessing is what [100:01] you'd reach for in order to speed up [100:03] CPUbound work. And if you're unsure of [100:06] what is what, then you can reach for a [100:08] profiler like we did earlier uh to see [100:11] what's going on under the hood and where [100:14] your code is spending much of his time [100:16] and um and what that time consists of. [100:19] So there's more and more async.io [100:21] compatible libraries popping up all the [100:22] time. Uh and the more libraries that are [100:25] built on top of it, uh the more it'll [100:27] allow us to use it in our own code. Uh [100:29] we have web frameworks like fast API, [100:33] HTTP clients like HTTPX and AIO HTTP. Uh [100:38] there are database drivers. Um SQL [100:40] alchemy supports asynchronous now and [100:43] there are asynchronous drivers for [100:45] Postgress uh MySQL and others. Um [100:49] there's also the AIO files for file IO [100:53] and uh many more out there. So it's [100:54] definitely a growing ecosystem. But with [100:57] that said, I think that's going to do it [100:58] for this video. Hopefully now you have a [101:00] good idea of how to use async.io in [101:03] Python. Uh even if we didn't get into a [101:06] specific example of what you were hoping [101:08] to see, like maybe asynchronously [101:10] connecting to a specific database that [101:12] you use or something like that. Um I [101:14] hope that you now feel like you at least [101:16] have more of a grasp on how this works [101:18] under the hood and also have a mental [101:20] checklist of how you'd prepare using [101:22] Async. [101:24] um and some ideas for how to use this in [101:26] your own projects. I'm going to be [101:28] putting all of the code and animations [101:30] from this video on GitHub and I'll also [101:33] add the animations to my website and [101:35] I'll put links to those pages in the [101:37] description section below. In a future [101:39] video, I'm going to be showing how to [101:40] use Fast API, which is a really fast web [101:43] framework that leverages async.io and [101:46] let you use the async await patterns [101:48] that we've learned here uh to build web [101:51] APIs that can handle many requests [101:53] concurrently. So hopefully this tutorial [101:56] will allow you to fully understand [101:57] what's going on there uh when we see [102:00] that in action. But if anyone has any [102:02] questions about what we covered in this [102:04] video, then feel free to ask in the [102:05] comment section below and I'll do my [102:07] best to answer those. And if you enjoy [102:08] these tutorials and would like to [102:10] support them, then there are several [102:11] ways you can do that. The easiest way is [102:13] to simply like the video and give it a [102:15] thumbs up. Also, it's a huge help to [102:16] share these videos with anyone who you [102:18] think would find them useful. And if you [102:19] have the means, you can contribute [102:21] through Patreon or YouTube. And there [102:23] are links to those pages in the [102:24] description section below. Be sure to [102:26] subscribe for future videos. And thank [102:27] you all for watching.