Python Tutorial: AsyncIO - Complete Guide to Asynchronous Programming with Animations
Key Takeaways
This video provides a comprehensive guide to asynchronous programming in Python using the AsyncIO library, covering concepts such as coroutines, tasks, and the event loop.
Full Transcript
Hey there. How's it going everybody? In this video, we're going to be learning all about Async.io in Python. Async.io is Python's built-in library for writing concurrent code. And it can seem a bit intimidating at first with all the different terminology and moving parts. But by the end of this tutorial, I'm hoping that you'll have a solid understanding of how it works and when to use it. Now, in this video, I'm going to be covering a lot. We're going to be learning how Async.io actually works under the hood. We'll see visually what's happening with some animations that I've put together. Uh we'll see how to update an existing codebase to use async.io. We're going to be able to determine when and where to use async.io with some simple profiling. And we'll discuss when to choose async versus threads versus multiprocessing. And the code in the animations that I'm going to use in this video are going to be available on my GitHub and website after this is out. So, if you all want to follow along with those yourselves, then I'll leave links to those in the description section below. Now, I'm going to get to some code as soon as I can, but there's a few basics that we have to get out of the way first. So, async.io is a Python library for writing concurrent code using the async await syntax. And I want to mention that we're using the latest version of Python in this video. And I'll be teaching the current way to do things. Async.io has evolved quite a bit over the years. Uh there have been different ways of running the event loop, scheduling and running task and all kinds of different changes. Uh but we're going to focus on the modern ways that you should be using today. Now before we dive too deep, let's talk about what concurrency actually is. So with synchronous code execution, uh which is what we normally write, one thing happens after another. So, it's kind of like going to a Subway restaurant where uh you put in your order and they make your entire sandwich from start to finish uh before moving on to the next customer. But with concurrent code, it's more like going to a McDonald's where someone just takes your order and then moves on to the next customer while your food is being made in the background. And that difference when applied to code can be really confusing at first for a lot of people. So here's something that I think is important to understand pretty early on. So asynchronous doesn't automatically mean faster. It just means that we can do other useful work instead of sitting idly by while waiting for things like network requests and database queries and stuff like that. That's why async IO excels at what's called IObound tasks which are anytime your program is waiting for something external. Now, Async.io is singlethreaded and runs on a single process. It uses what's called cooperative multitasking where tasks voluntarily give up control. For CPUbound tasks that need heavy computation, you'd want to use processes instead. And we'll see how to tell the difference between IObound and CPUbound here in a bit. But before I lose your attention with too much theory early on, let's go ahead and jump into some code and we'll learn the basic terminology that we need to know as we go. So, let me walk through this code and explain the terminology as we go. Now, the terminology is what really trips a lot of people up when they first start learning async.io because there's quite a bit to remember. Now, right now we have some asynchronous code here that we'll walk through and explain, but for now you can see that we have a simple synchronous function right here at the top and that just sleeps for a bit and then it returns a string. We're running this synchronous function inside of our main function here. Our main function is asynchronous and you can see that it has this async keyword. Since it's an asynchronous function, we can't just call it directly. Uh in order to run our main function, we have to start what is called an event loop. And we're doing this down here at the bottom with async io.run and passing in uh that main async function there. So the event loop is basically the engine that runs and manages asynchronous functions. Think of it a bit as auler. It keeps track of all our tasks and when a task is suspended because it's waiting for something else uh control returns to the event loop which then finds another task to either start or resume. So we have to be running an event loop for any of our asynchronous code to work. That's what this async io.run function is responsible for. It's getting the event loop running task until they're marked as complete and then closing down the event loop whenever it's done. So let me go ahead and run the code that we have right now. And we can see that we're just printing out that this is a synchronous function. And then we get that result. So we came in here, ran this event loop. This is an asynchronous main function. And we are just calling this synchronous code here within our async function for now. But we don't want to just run synchronous functions inside of our event loop. We want to use concurrency. to use concurrency. We're going to be seeing this await keyword a lot. So, let me uncomment the futures code here uh so that we can talk about awaitables. So, let me uncomment this here. So, you can see here that we're using this await keyword. So, awaitables are objects that implement a special await method under the hood. You're going to see await everywhere in asynchronous code. an object has to be awaitable for us to use that keyword on it. Now, why can't we await a synchronous function uh like this sync function here or something like timesleep? Well, synchronous libraries don't have a mechanism to work with the event loop. They don't know how to yield control over and resume later. So, basically synchronous code like time.sleep sleep or our synchronous function here. Uh they don't have that uh underlying special await function that they need in order to pause their execution and start back later. Uh these things need to be coded in to be compatible with async io. And that's why we can't await time. And we need to use something like async io.sleep instead. And to use this await keyword, we also have to be within a function that has this async keyword. So if I remove async from our function there, now you can see that I'm getting a warning here. And if I hover over this, uh, it's not letting me hover right now, but we can see that my rough warning here is telling me that await should be used within an async function. So we have to be within an async function in order to use these await keywords. Okay. So what does await do? So when you await something, you're basically telling the event loop to pause the execution of the current function and yield control back to the event loop which can then run another task and it'll stay suspended until this awaitable completes. So in Python's async io there are three main types of awaitable objects. First there are co- routines which are created when you call an async function. Second there are tasks and tasks are wrappers around co- routines that are scheduled on the event loop. And the third there are futures and futures are low-level objects representing eventual results. Now, if you're coming from somewhere like the JavaScript world, uh futures are a lot like promises in JavaScript. They're a promise of a result that will be available later. But unlike JavaScript, in Python, we almost never work with futures directly. We write co- routines and when we schedule them as tasks, async.io uses futures under the hood to track those results, but we won't be seeing them much in this video. you're really only going to use futures directly if you were writing low-level async IO code. Uh like if you were building an Async compatible framework, but just to show you what they look like, let me run this example with uh this futures example here really quick. And uh just so we can see what's happening. So a future's job is to hold a certain state and result. The state can be pending uh meaning the future doesn't have any result or exception yet. Uh it can be cancelled if it was cancelled using future.canc uh or it can be finished and it can be finished uh by a result being set by uh future set result or it can be an exception with future set exception. So you can see here after we created this future and printed that out, it says that future was pending after we created it. And then we set the result right here to future result test and got that result by awaiting it. And then we printed that out. But like I said, this is lower level stuff and we won't be using it directly in this video. We're going to be working mostly with co- routines and tasks. So I'm going to delete this future example here and then we're going to look at co-outines and tasks. So let me uncomment the co-outine example here. And co-outines are functions defined with the async defaf keywords here. So main is a co-outine here. We have async defaf. If I go up here, we have this async function and I have a comment here that says also known as a co-ine function. So we have this async defaf async function. And within here, it's a lot like our synchronous function, but instead of using time.sleep, sleep we are using async io.sleep and we are awaiting that as well. So that's what this co-outine object is right here. So co- routines are basically functions whose execution we can pause and there's actually two terms here that we need to understand. There's the co-outine function which is what we define with the async defaf keywords and then there's the co-outine object which is the awaitable that gets returned when you call that function. So this is the co-outine function here and after we call that function this is the co-outine object here. So co- routines are a bit like generators in the sense that they can suspend execution and resume later but they're designed to work with an event loop. They have extra features that Async IO needs to schedule them, await IO, and coordinate multiple tasks. So, let me go ahead and run this here so that we can see what's happening. Now, again, if you're a bit confused right now, I think all of this will make a lot more sense once we look at the animations that I've put together. Uh, but right now, we're just focusing on learning these terms so that we can understand a bit better what we're looking at once we get to those animations. So you can see here when we executed this co-ine function, it doesn't run all of that function. It uh didn't come in here and print out that this was a synchronous function um before we got to this line here. It just created this co-ine object and then we printed out that co-ine object which is right here. So when we ran that co-ine function we got this co-outine object and to actually run this co-outine and get the result we have to await it. When I await that co-outine object we can see that that's whenever it runs the print statement from our asynchronous function and then we got that result and printed that out and that result was just async result test. Now when we await a co-artine object directly like this, it's both scheduled on the event loop and run to completion at the same time. Okay. So now let's look at tasks. So I'm going to comment out this co-ine section here. And now let's look at tasks here. Now tasks are wrapped co-outines that can be executed independently. Tasks are how we actually run co-outines concurrently. When you wrap a co-outine in a task using async io.create task like we've done here, it's handed over to the event loop and scheduled to run whenever it gets a chance. The task will keep track of whether the co-outine finished successfully, raised an error or got cancelled just like a future would. And in fact, tasks are futures under the hood, but with extra logic to actually run the co-outine and do the work that we want to do. That's why we work with tasks instead of futures uh in most of our code. But unlike co-outine objects, tasks can be scheduled on the event loop and just sit there without being run until the loop gets control. And this is the key to async IO. You can queue up multiple tasks at once and then the event loop will be able to run them whenever it's ready. Uh letting them take turns while waiting on IO. So let me go ahead and run this so we can see this basic example here. So you can see that when we uh printed out the task that we created here it shows that the task is pending. It shows us uh the name of the task here and the co- routine that it is wrapping. And when we await that task it runs that co- routine and we get those print statements and that returned result. Okay. So that does it for the terminology. Uh now let's see this in action with some specific examples and some animations. Uh I don't know about you all but I'm a very visual learner. So seeing this stuff in action helps me a lot more than just looking at the code. So first I'll show the Python code and then we'll see what's happening under the hood with an animation. So here is the Python code here. Now in this first example we're not going to be using async IO at all. This is just normal synchronous code with no event loop or anything like that. So we should know exactly how this works. So if we go down and uh look at the code here, we are resetting setting the results equal to this main function here. And don't worry, I have some extra timing functionality here um just so we can see how long this takes. Uh but then we're running that main function. The main function comes in here and we are running this fetch data function with a value of one. Fetch data then comes in here, prints out that we're doing something with one. Then it sleeps for that 1 second that we passed in. Then it prints that we're done with one and then returns the result of one. Okay? And then after that returns, that return value gets set to that variable there. Then we print that fetch one's fully complete. And then we come here and do result two is equal to fetch data two. That comes up here and says do something with two. Times sleep for two. Done with two then returns result of two to this result two here. We print out that fetch two is fully complete. And then we return both of those results. So fully synchronous code. We should know how this works. Let me go ahead and run it. And I added some timing code here that'll show us how long this took. So we can see it ran through that code synchronously and it finished in 3 seconds. And that makes sense because we're doing fetch data for 1 second and then we're doing fetch data for 2 seconds. So 3 seconds total. So now let me show this as an animation in the browser so that we can get an idea of what these animations are going to look like. And I've taken the timing code out of these animation examples so that we can just pay more attention to the actual code. Okay. So, let me go to example one here. Sorry about that. Now, hopefully this text is large enough for you to see. I made this as large as I could while everything can still fit on the screen here. Now, if you're walking through these examples, uh, these animations with me by using my website or have downloaded these yourself, then I've set this up so that the right arrow key progresses to the next steps. And unfortunately, I didn't add uh any functionality to go backwards. So if you want uh the animation to run again, then you'll have to reload the page. Okay. So like I said, all of this is synchronous code here. So we're just going to walk through this. It's going to uh see those functions there. Then we're going to uh run result equals main there. It's going to go into the main function and none of this is going to kick off anything on the event loop. We are going to run fetch data with the parameter of one. that's going to come in and run some print statements. We're going to do a time.leep. Now, time. Is going to kick off some background IO here and sleep for one second. And it's going to stay on this line for that entire second until that completes. Once that completes, then we can move forward and do our other print statements here. Return that. And then we're going to walk through do result two is equal to fetch data to. Same thing. We're printing out some uh text there. We're going to run this background IO. It's going to sleep for two seconds now. And that's going to stay there until that completes. Once that is done, then we can come in and print out our other statements. And then we return our result one and two. And then we come down here and print the results that we got from main, which was just uh a list of those two results. Okay, so that's synchronous code. That should be what we expect. But since it was synchronous, we were waiting around during those sleeps when we could have been allowing other code to run. So now we might want to improve this performance and switch over to using async IO. So let's move on to example two here and see an example of how someone might go about doing this. Now in this example, we're going to see what a first attempt at converting our code to asynchronous might look like. But there's going to be a common mistake here. Uh so you can see that we've converted our functions into co- routines. So we have uh an async defaf fetch data here. Our main is async defaf co-outine here. And then we are running this in an event loop here with async io.run. So this is what someone's first attempt might look like to go asynchronous here. So we are setting our tasks here directly to our co-outine objects here. Fetch data 1 and fetch data 2. Almost like we're just calling a function. This is very similar if we go back to example one how we were setting the result equal to fetch data 1 and fetch data 2. Uh that's what we're doing here with these tasks. And then we're trying to get the result here by awaiting those co- routines directly. And then after we await one, we're printing out that task one is fully complete. After we await two, we're printing out that task two is fully complete. Now, we could have awaited these directly. I could have said that result one is equal to await uh fetch data one like that. uh but I broke them up here into uh being able to see the co-outine object first and then await that separately. Okay, so let's run this and see if this works. So we run this code and we can see that it still takes 3 seconds. So we're not getting any concurrency benefit here at all. So why is that? Well, some people have a misconception that when you run a co-outine function like we did here that it creates a task and schedules it. But it doesn't. It just creates the co-outine object. So when we await that co-outine object, we're scheduling that and running it to completion at the same time. We get no concurrency here and no benefit to using async.io. So, let me show you what this looks like in an animation that I put together, and I think this will make more sense. So, here we have that same code that we had before. So, we're going to run through this. Now, once we get to results equals async io.run main that is going to create our event loop. So, now in our event loop, right now we have one co-ine. We have this main coine that is going to run. So now the code uh whenever I step forward here it's going to run from this co-outine here. So as I go through this now we're saying task is equal to fetch data 1 that's going to create a co-outine object. Task two that's going to be another co-outine object. And now when we do result one equal to await task one that is going to schedule that on our event loop and run it to completion at the same time. So if I step forward here then our main co-outine is suspended. So await is what suspended our main co-ine and now our event loop is looking for tasks that are ready. Now we just scheduled that fetch data one task uh whenever we ran this co- routine directly. So our event loop is going to see that and say okay I have a ready task here. So, let me go in here and run this until I hit an await. So, now it's going to come in here. It's going to print that we're doing something with one and then we are going to await that sleep. And once we hit that, it's going to suspend our current task. And it's going to uh be suspended until async.io.sleep is complete. So that's going to kick off our background IO here with our timer. And then that's going to suspend. And now that's going to stay suspended until our timer is complete. Once that timer is complete, then this is going to wake up this task and say, "Hey, this what you were awaiting here is complete. So now you're going to be ready to run again." So now that completed, now we're ready to run again. Our event loop is going to go through. see that we have a ready task here and then it's going to go back in and it's going to continue running from where it left off. So then we're going to go through here, print out these other statements and finally we're going to return here and once we return now this task is complete. this fetch data one task is complete and that is going to wake up our main co-outine here because now this task one that we were waiting for is now complete. So now main is ready to run again. Our event loop is going to see that. It's going to come in here and run it and now print out task one fully complete. And now we're going to do the same thing with await task two. It's going to suspend our main co-outine. It's going to uh schedule our task two here onto our event loop and also run it to completion. So, we're coming in here and we are printing these out. This await async io.sleep here is going to suspend this task until this async.io.sleep is done. So, it kicks off that timer. It suspends our task. That timer eventually is going to complete. Once that's complete, then our task can wake up here. And now it's ready to run. The event loop is going to see that. Run it where it left off at this await statement. And then we're going to walk through our other print statements here. Hit our return statement. And that's going to complete that task. And now since task two is complete, and that's what we were awaiting there. Now our t now our main co-outine is ready to run again. So now our event loop's going to see that we have a ready task here, come in and finish printing these out. And then finally it will return that main co- routine and close all of that down. And then back here in our main Python code, uh, we get those results and we can print those out. So I hope that that made sense. Uh, let me reload this really quick. I won't go through the entire animation again, but the one thing that I really want you to catch on to here is that whenever we created these co-outines here, um they are not scheduling any tasks on our event loop. They are just returning a co-outine object and then here when we await those co-outine objects directly, we are both scheduling them and running them to completion at the same time. Uh so that is why we are not getting concurrency. We only have one task down here that is ever running at a time. So uh basically we have the same performance that we had when we ran our synchronous code. So let me go back to our code here. And now let's look at example three. And this is going to be a look at one of the correct ways to run asynchronous code. Now the only thing that we've changed here from the previous example is that now we're we are creating tasks from these co-outines using async io.create task instead of just calling those co-outines directly. Now when we create a task it schedules a co-outine to run on the event loop. This is the part that we were missing from the previous example. So now if I run this then we can see that in our output that do something with one and do something with two ran one after another without the first task finishing and before task one was able to print out that it was done or uh task one was fully complete. It immediately came in here and said okay I'm going to do something with one. I'm going to do something with two. And then since our uh number one task only slept for 1 second, it completed first. And then our second task completed um after that since it's sleeping for 2 seconds. And we can see here that the total time of our script here uh took 2 seconds in total. And that is because it ran both those at the same time. And that total time is simply how long the longest running task was, which was two seconds. So we did get concurrency here. So let's see this in an animation uh so that we can see exactly what that looks like. Okay. So now here we can see that I have the code that uh creates the task here. So let me walk through this and again we're going to get to this line here where we are running our event loop and we're going to run this main co-outine. So that main co- routine is now running on our event loop. And then when we get to this point here where we create this task with fetch data one that is going to schedule that task on the event loop. So now we can see that that task is now scheduled and ready on the event loop. Uh and now in our main co-outine here we're still going forward. And now with task two with async.io.create create task that is also going to schedule that fetch data too on our event loop. So now we can see that that gets scheduled as well and both of those are ready. And now when we get to this line here result one equals await task one. This await is going to yield control over to the event loop and it's going to suspend our main co-outine here until this task one is complete. So if we go forward with that, our main co- routine suspends here. And now our event loop is going to look for any ready task. It's going to see that we have fetch data one ready. So it's going to come in. It's going to run this. And then it's going to hit an await statement here. And now it's going to suspend uh this task until async.io. So it's going to kick off that async.io. sleep here in the background. And now it's going to suspend that task. Now, this is where we get concurrency since we had both of these scheduled. Now, our event loop is going to keep looking for tasks that are ready. It's going to see that we have this fetch data 2 here. And now, it's going to come in and run this. So, we're going to do our print statements here. We're going to hit our await, which is going to suspend our fetch data 2 co-ine. And it's going to be suspended until async.io sleep is done. And that's going to be the one for two seconds. So, it's going to kick that off. Then, it's going to suspend. And now you can see that this is the concurrency here. We have both of these timers running here in the background. So, these are all going to stay suspended until something gets finished. So, our first timer completes, it's going to wake up our first task here. And now that that first task is ready, our event loop is going to find a ready task. And then it's going to pick up where it left off and just print that we are done with one and then return that value. And now once that is complete, remember that our main co-outine is awaiting that task one. So as soon as this task one is complete, then our main co-outine is now ready to run again. So it's going to come in here and print out that task one is fully complete. We're going to await our task two. Now, task two is already suspended. So, there's nothing really to do here other than to wait for this timer to finish here. Once that timer is complete, it's going to wake up this fetch data 2 task here. Now, our event loop is going to see that that's ready, come in here where it left off, and print out that we're done with two. Return the results. And now that this is complete, our task two, it's going to tell our main co-outine that it's ready to run. Our event loop is going to see that and run where that left off. Print that task two is fully complete and return a list of those results. And then that event loop is going to close down. We have those results there. We can print those out and we have everything there. So I hope that that example makes sense and now it makes sense why uh this code worked here by scheduling these tasks ahead of time uh instead of whenever we awaited these co-outines directly because when we awaited these co-outines directly it didn't get scheduled until we hit this await statement. So we only had one task scheduled and run fully to completion here. Uh in our second example, we had both of these tasks scheduled. And then when we awaited, it was able to uh run our task one until it hit an await. And then once we hit that await, then it was able to go through and see that we had another task that was ready and scheduled and it could run that as well. Okay, so I hope that that makes sense. Uh the more examples that we see, I think the more clear that this is going to be. Now in our fourth example here, now I want to show you uh an example of something to show you something important about awaiting tasks and how things are actually run on the event loop here. So I haven't changed much with this code here. It's basically all the same. But all I did here uh from example three uh in example three I'm awaiting task one first and then printing out that task one is fully complete. In example four, I am setting result two and I'm awaiting task two first and printing out that task two is fully complete and then I am awaiting task one and saying that task one is fully complete. Now, what do you think is going to happen here when I run this? So, some people might think that we're going to run task two first and then task one or maybe run them both at the same time, but that task two will complete first. But let's see. Let's go ahead and run this and see what happens. So, when I run this, then our results might be a little confusing to some people. So, we still finished in 2 seconds. So, it's still running concurrently. But if you look at the output, task one still runs first. There's no change there. The only difference is that it didn't move to our task two fully completed uh print statement here until task two was completely done. So that's what I want you to take away from this specific example is that when we await something, we're not guaranteeing that we run that particular part right at that moment. uh the event loop is going to run whatever is ready. What we are guaranteeing is that we're going to be done with what we awaited before moving on. And actually, it doesn't even need to be one of these tasks that we await. Uh so, for example, I could use async.io. Instead, and that would also yield control to our event loop, and those tasks would still run in the same order. Uh, and it would just wouldn't move on until our async io.sleep is done. So, let me do that and just show you what I mean. So, I'm going to await async.io. Since our longest sleep is 2 seconds, then I'll just sleep here for 2.5 seconds. So, if I run this, then it's going to be the same output pretty much. Uh, except now we don't have a second result because await async.io. Just returns none. So our result two there is none. Uh and it finished in 2.5 seconds since that's now a the uh longest sleep that we have. But we can see that the order of execution basically remains the same. It still did something with one first then two got done with one got done with two and then once this awaited async iosleep was finished here that is when we moved on to printing out that task two is fully complete there. So that is what I wanted to show you with that example. Let me also show you this here in an animation just to really hammer that point home. So I'll start stepping through the first part of this pretty quickly now. Uh so we're going to get down to where we run our event loop with that main co-outine and then that is going to create our first task there with task one. It's going to schedule that. Our task two with create task is going to get scheduled as well. And this time we are waiting task two first. Now when we hit await, what it's going to do is it's going to suspend this coine until task two is done. So we're suspending and we're yielding control back over to the event loop. The event loop is going to go and see what tasks are ready. Now this uses a FIFO Q in the background, which is first in, first out. That's not super important, but uh uh what is important is just to know that um what you're awaiting isn't always going to be the first thing that gets run. It's just going to be whatever the event loop has ready. So right now it is this fetch data task one here. And that is going to run until it hits its await statement and suspends itself and kicks off that background sleep. And now we have another task ready here. The event loop's going to see that going to come in until that hits its await statement. It's going to kick off that background sleep and it's going to suspend itself until one of these timers is done. Then this timer is done here. It's going to wake up our first task here. And now this is where something a little different happens. Uh that was different than our previous example. So it's going to see that this is ready. It's going to come in and pick up where it left off here. We're going to print that we're done with one and return that result and that is going to complete. Now before we were awaiting task one here. So before once this task one was done then our main co- routine was going to say okay I'm ready to run again. But that's not what we awaited. We awaited task two. So what this is going to do is it's just going to save that result in memory for now. And now we still just have two suspended co- routines here. So these are going to stay suspended until this timer completes here. That completes, it's going to wake up this second task. And then our event loop is going to see that that is ready. It's going to come in and do its print statements that it's done with two. It's going to return that result. And now our main co-outine is going to get woken up whenever this task two is done here. So now it is saying that it's ready. And now we move forward with our print statements. So we're going to print that task two was fully completed. When we do this await task one here, that's already been completed. So there's nothing left to do there. All it's going to do is pull that result from memory that it has saved. So it's just going to set that variable equal to what that uh return value was. We're going to print out that task one is fully completed. and then return a list of those results. Close down the event loop and move through and print out all of those. Okay, so I hope that this is making more and more sense as we're seeing more and more examples here. Okay, so moving on to our next example. Our next example here is going to be really important. So let me pull this one up here. Now in this example, we're going to see what happens if we block the event loop with synchronous blocking code. So this is pretty much the same as the examples that we've been looking at where we are creating tasks, scheduling those on the event loop and then awaiting those tasks. But in our fetch data co-outine here, instead of using async io.sleep, I'm using time.sleep here. Now time itself isn't awaitable. So I can't await that here. If I put in await, then that's just going to throw an error. Um, but what we can do is we can run this inside of an asynchronous function like fetch data and we can schedule that on our event loop and we can await that co-outine. But this is bad practice here and we're going to see exactly why. Uh, because like I was saying before, time.leep isn't awaitable and it wasn't coded to know how to suspend itself and yield control over to the event loop. But what happens if we put that blocking call there and then schedule and run that co- routine? Well, let's go ahead and see. So, I'm going to go ahead and run this. And we can see that it did something with one, did something with two, uh, task one fully complete, task two fully complete, and we finished in 3 seconds here. So, since we finished in 3 seconds, we know that those didn't run concurrently. We can also see that it didn't start both of these at the same time either. we came in and did something with one and it wasn't until we were done with one that it moved on to doing something with two. And that's because time.leep blocks the event loop. Now, that might not be obvious and some people might think that uh just because we had that synchronous code being run inside of a task that we assume that maybe somehow it would have worked. Um, but let me show you what's actually going on here and why this blocks. So I'm going to pull up our fifth example here and let's go ahead and run through this to see what happens. So I'm just going to go down to the part where we start our event loop and run that main co-outine there. And now within the event loop, we are scheduling our task here and creating those. And now we're getting to the point here where we are awaiting task one. So that's going to suspend our main co- routine and it's not going to pick back up on our main co-outine until task one is complete. So I'll go ahead and move forward here. That's going to suspend. Our event loop is going to find a task that is ready to run. It's going to come in here into fetch data one and it's going to run down through this. We're going to print that we're doing something with one. And now we're going to get to this time. Now here it's going to kick off that background IO here. But what's going on is that we never awaited here. So this um task never got suspended. So it's just going to sit here until this blocking code is done until this background IO is done. So what we have here is a blocked event loop. So eventually that sleep is going to complete. Um there's nothing to wake up here because we're still just um running synchronous code. So now that that's complete, we can run forward with our task here. Say that we're done with one. Return that result. Our main co-outine is going to wake up here. Now again, like I was saying before, even though our main co-outine is now ready because that task one was complete, that doesn't necessarily mean that that is the next thing that the event loop is going to run. Uh the event loop uses that FIFO first in first out Q in the background. And since task two has been ready for a while and was ready before main became ready again, then it's going to actually find this task two first. And this is where my animation is lacking a bit because it doesn't show that FIFO order of the ready Q. Uh but that's what's going on there. So it's going to see that our task two is ready here. We're going to come in and run this. We're going to print out a couple of things. We're going to get to this time. That timesleep is going to kick off our background IO. Um, but it does not know how to suspend itself. Uh, so it's just going to hang here until this sleep is complete. Once that sleep is complete, then this can continue on and print that we're done with that. It's going to return that result. Now, our main co-outine here has been ready for a while. our event loop's going to see that. It's going to come in and print out the rest of these print statements. Close down the event loop and then we will print out our results there. Now, let me restart this animation really quick. And I'm just going to go back to a certain part of this animation. It's where we completed our first task here. And now both of these tasks are ready. And like I said, this is a FOQ where it's going to find this first this uh task number two first and run this. Now, you might be thinking that I should be emphasizing the order in which these run a little bit more and kind of explain that a little bit more, but to be completely honest, you don't really want to get bogged down in what exactly is running on the event loop at any one time or what's going to be next or anything like that because async.io, So it's really meant we're going to be running you know tens possibly even hundreds of things concurrently and the event loop is going to handle everything that is ready. Uh when we run real asynchronous code it's not going to be cut and dry examples like we have here where we know exactly when most tasks will be finished and when others will be ready. Um, so we don't have control over that and we shouldn't uh want to have control over that. The event loop is just going to do its job. What we do have control over is not moving forward until something is done. So if I um, you know, didn't want this to move forward until task two was done, then I would put an await task two there. Um, but in terms of whether this goes back and runs the main co- routine or fetch data first, that shouldn't really matter that much to us. Uh, let the event loop run whatever tasks are ready. And if we really want to enforce uh, anything, it'll be that we're going to enforce exactly when something is done, not, you know, exactly whenever it gets its turn in the event loop. So, I hope that that makes sense. Um, I just kind of thought of that as I was walking through that example uh last time. Um, but with that said, let me go ahead and go back to our other examples here. So, this example five, the one that we just saw where we have this time. Uh, blocking our event loop, this is exactly what happens when we run any blocking code in our asynchronous functions. So, while we're using time.sleep asleep in this example. This could easily be any other code. This could be uh request.get making a web request or any other synchronous code. Uh because requests uh the request library, it's not asynchronous. So to do web requests asynchronously, uh we would need to use an asynchronous library like HTTPX or AIO HTTP. Uh but if we do have some blocking synchronous code that doesn't have an async IO alternative then we can also use async IO to pass this off to threads or processes and the event loop will manage those threads and processes for us and that's what we're going to see in the next example. So let me open up example six here. Now, this example is going to be a bit more advanced here, but I want to show this since it might be something that you'll see or even need to do when working with some asynchronous code. So, right off the bat, let me explain a couple of the changes that I made here that are specific to this example since we're using threads and processes. So, first, instead of running our synchronous blocking code inside of an asynchronous function, uh, which we don't want to do, I've instead just turned our fetch data function back into a regular non async function. So, it's just a regular synchronous function. And we'll use async.io to pass this regular function to a thread. And then we'll also see an example of how to pass this to a process. Now, you'll notice here that with these print statements, I put flush equal to true argument in there. Uh, that's just to make sure that our print statements come out in the order that we expect. Sometimes when running these outside of our current thread, uh, print statements can get buffered and come back in a seemingly weird order. So, that's more just for the tutorial here. Now another thing down here at the bottom where I am starting up our event loop here you'll notice that I'm also using this if name is equal to main conditional and this is for our multi-processing example. So when Python spawns multiple processes it needs to rerun our script in that new process. So uh this check makes sure that we don't end up in an infinite loop whenever it uh runs our code and spawns that new process. Okay. So with that said, let's see how we can do this here. So we still have our asynchronous uh main function here. And this is going to look very similar to what we were doing before. Uh except when we're creating our task, we're simply wrapping our fetch data function here inside of this async io.2thread function. Uh this will wrap our synch synchronous function with a future and make it awaitable. Now you'll notice that I didn't execute the synchronous function. I didn't say you know fetch data with parenthesis here and pass in that one. I don't want to do that. You want to pass in the function itself and its arguments separately uh to this async io.2thread function so that it can execute later when it's ready. Then we're just awaiting this just like any other task. Now for processes here this is a little bit more complicated. So first we have to import this process pool executor up here from concurrent.futures and then we have to get the running loop uh because we are using this loop.runinexecutor method here. So here we're just saying loop is equal to async.io.getrunning loop and then within this process pool executor we are creating these tasks with loop.runinexecutor run an executor passing in this process pull executor here. And again, just like before, we're passing in the function that we want to run in a process and the arguments separately. And just like with our threads, that's going to wrap that new process in a future that we can then await. And once we've done that, uh, we're simply awaiting those like we did before. So let me go ahead and run this and see what we get. So we can see that that works and that these ran concurrently. Uh it took 4 seconds, actually a little bit over 4 seconds because threads and processes have a bit of overhead to spin up and tear down. Now the reason it took 4 seconds and not 2 seconds is because we ran these in two different groups of two. We ran uh both of our tasks in threads and then we ran both of our tasks in processes there. And since the longest task is 2 seconds, uh we ran our threads took 2 seconds there running concurrently. And then our processes took 2 seconds running concurrently as well. So just like we've been doing so far, uh let me pull this up here in the browser and run through this in an animation just to really knock this point home. Uh, this code is a little bit longer here. So, we can see that some of this gets cut off. Um, but let me go ahead and run through here. I'll scroll down to where we can see that we're starting up this event loop with that main co-outine. Okay. And now we come in here. We are creating this task. We're passing off fetch data with an argument of one to a thread and we're creating a task out of that. So, that gets created and scheduled. We're doing the same thing with fetch data and an argument of two there that gets scheduled on our event loop. And now when we await task one, it's going to suspend our main code routine here until that task one is complete. So now it's going to find our thread here. Now I don't have the code here for our thread. And the reason I'm not showing our synchronous code in this task is because that code isn't running in our current thread anymore. Uh that's going to go off and run in its own thread. So eventually uh what that's going to do is that task is going to hit and await um something that this async io.2 thread puts into place for us. And then it's going to kick off that background thread and run our synchronous code for us. So this thread gets started here running that synchronous fetch data code. It's going to suspend that task and then we might see some print statements coming in here while that thread is running that synchronous code. But now our event loop is free to move on to our other task here. It's going to run our other bit of synchronous code there in another thread and suspend this second task here. And now both of these threads are going to be running in the background. And then eventually that is going to complete. This thread is going to be done. It's going to notify our twothread task here. And that's now going to be ready. Once that is ready, then it's going to return and complete. And then it's going to wake up our main co- routine here. Since we were awaiting that task one, it's going to print that that's fully completed. We're going to move on to waiting for that task two thread to be done. So again, we've seen all this before. That completes. That's ready. That completes. That's ready. So a lot we're doing a lot of the same stuff here. So I'm going to keep going through this a little bit faster now because we're kind of should be kind of used to this as we go. Now we're awaiting task one. Task one's going to come in here. Run. That's going to be a process in the background IO here. It's still printing stuff out out here. We're running another process in the background IO. Eventually, that's going to complete. That's going to complete there. Our main co-outine is going to pick back up, do some print uh do some print outs there, wait for that second task to finish there. Once it finishes, it wraps up and completes. And then we move on with printing out all of our code here. Now, I know that that example was a bit more complicated, but I wanted to show that because if we're using async io, there might be times when we don't have an asynchronous option in order to get concurrency, and we'll need to run some blocking code in threads or processes. Uh, but now let's go back to our more standard use cases here. So this is our last example here before I get on to a uh real world example and we can uh see how to update a real codebase to asynchronous. So with example seven here in this example we're going to see other ways that we can schedule and await tasks. So so far we've been creating tasks and uh one at a time and awaiting them manually. But a lot of times we might want to create a bunch of tasks and run them all at once. We can do this with either gather or with task groups. So here our first section here uh I've just taken out some print statements along the way. But our first section here is basically what we've been doing. We're
Original Description
In this video, we'll be learning all about AsyncIO in Python and how to write asynchronous code using the async/await syntax. We'll explore how AsyncIO works under the hood with visual animations, understand key concepts like coroutines, tasks, and the event loop, and see how to convert existing synchronous code to use AsyncIO effectively. We'll also cover how to use profiling to identify optimizations, when to choose between AsyncIO, threads, and multiprocessing, and work through a real-world example that demonstrates dramatic performance improvements. By the end of this video, you'll have a solid understanding of asynchronous programming in Python and know exactly when and how to use AsyncIO in your own projects. Let's get started...
The code from this video can be found here:
(Use the right-arrow on your keyboard to step through animations)
Animations Repo - https://github.com/CoreyMSchafer/AsyncIO-Animations
Animations - https://coreyms.com/asyncio/
Code Examples Repo - https://github.com/CoreyMSchafer/AsyncIO-Code-Examples
UV Tutorial: https://youtu.be/AMdG7IjgSPM
Ruff Tutorial: https://youtu.be/828S-DMQog8
✅ Support My Channel Through Patreon:
https://www.patreon.com/coreyms
✅ Become a Channel Member:
https://www.youtube.com/channel/UCCezIgC97PvUuR4_gbFUs5g/join
✅ One-Time Contribution Through PayPal:
https://goo.gl/649HFY
✅ Cryptocurrency Donations:
Bitcoin Wallet - 3MPH8oY2EAgbLVy7RBMinwcBntggi7qeG3
Ethereum Wallet - 0x151649418616068fB46C3598083817101d3bCD33
Litecoin Wallet - MPvEBY5fxGkmPQgocfJbxP6EmTo5UUXMot
✅ Corey's Public Amazon Wishlist
http://a.co/inIyro1
✅ Equipment I Use and Books I Recommend:
https://www.amazon.com/shop/coreyschafer
▶️ You Can Find Me On:
My Website - http://coreyms.com/
My Second Channel - https://www.youtube.com/c/coreymschafer
Facebook - https://www.facebook.com/CoreyMSchafer
Twitter - https://twitter.com/CoreyMSchafer
Instagram - https://www.instagram.com/coreymschafer/
#Python #AsyncIO
Watch on YouTube ↗
(saves to browser)
Sign in to unlock AI tutor explanation · ⚡30
Playlist
Uploads from Corey Schafer · Corey Schafer · 0 of 60
← Previous
Next →
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Web fonts using CSS Font Face
Corey Schafer
Using Font Awesome in Desktop Applications (OS X)
Corey Schafer
Sublime Text 2: Setup, Package Control, and Settings
Corey Schafer
ArcGIS API for JavaScript Part 1: Our First Web Map
Corey Schafer
Mac Tip: Windows' Snapping Feature on Mac with HyperDock
Corey Schafer
Linux/Mac Terminal Tutorial: Creating Aliases for Commands
Corey Schafer
ArcGIS API for JavaScript Part 2: Starting Templates
Corey Schafer
Paver Patio Time Lapse
Corey Schafer
Mac Tip: Ways to perform Screen Capturing and Screenshots
Corey Schafer
WordPress Plugins: Imsanity
Corey Schafer
WordPress Tips: Test your theme with Theme Unit Test and Monster Widget
Corey Schafer
Sublime Text 3: Setup, Package Control, and Settings
Corey Schafer
Understanding Binary, Hexadecimal, Decimal (Base-10), and more
Corey Schafer
Mac Tip: Adding Folder Stacks to the Dock
Corey Schafer
CSS Tips and Tricks: Add External URLs to Print Stylesheets
Corey Schafer
JavaScript Arrays: Properties, Methods, and Manipulation (Part 7 of 7)
Corey Schafer
JavaScript Arrays: Properties, Methods, and Manipulation (Part 1 of 7)
Corey Schafer
JavaScript Arrays: Properties, Methods, and Manipulation (Part 5 of 7)
Corey Schafer
JavaScript Arrays: Properties, Methods, and Manipulation (Part 4 of 7)
Corey Schafer
JavaScript Arrays: Properties, Methods, and Manipulation (Part 3 of 7)
Corey Schafer
JavaScript Arrays: Properties, Methods, and Manipulation (Part 2 of 7)
Corey Schafer
JavaScript Arrays: Properties, Methods, and Manipulation (Part 6 of 7)
Corey Schafer
Python Tutorial: if __name__ == '__main__'
Corey Schafer
Sublime Text Quick Tip: "Go To Definition" Click Shortcut
Corey Schafer
How to quickly create favicons for the desktop, Apple/Android devices, tablets, and more
Corey Schafer
Easily Resize Multiple Images Using Picasa
Corey Schafer
Easily Resize Multiple Images Using the Mac Terminal
Corey Schafer
Python Tutorial: virtualenv and why you should use virtual environments
Corey Schafer
Python Tutorial: pip - An in-depth look at the package management system
Corey Schafer
Git Tutorial: Using the Stash Command
Corey Schafer
How Software Engineers, Developers, and Designers can volunteer their skills
Corey Schafer
Git Tutorial: Diff and Merge Tools
Corey Schafer
Git Tutorial: Change DiffMerge Font-Size on Mac OSX
Corey Schafer
Sublime Text Quick Tip: Launch Sublime Text from the Terminal
Corey Schafer
Python Tutorial: str() vs repr()
Corey Schafer
Programming Terms: DRY (Don't Repeat Yourself)
Corey Schafer
Programming Terms: String Interpolation
Corey Schafer
Programming Terms: Idempotence
Corey Schafer
Python Tutorial: Namedtuple - When and why should you use namedtuples?
Corey Schafer
Programming Terms: Mutable vs Immutable
Corey Schafer
Python Tutorial: Else Clauses on Loops
Corey Schafer
Overview of Online Learning Resources
Corey Schafer
Mac OS X Terminal Tutorial: Time-Saving Keyboard Shortcuts
Corey Schafer
Git Tutorial for Beginners: Command-Line Fundamentals
Corey Schafer
Quickest and Easiest Way to Run a Local Web-Server
Corey Schafer
Python Tutorial: Generators - How to use them and the benefits you receive
Corey Schafer
Python Tutorial: Comprehensions - How they work and why you should be using them
Corey Schafer
Chrome Quick Tip: Quickly Bookmark Open Tabs for Later Viewing
Corey Schafer
Programming Terms: Combinations and Permutations
Corey Schafer
Git Tutorial: Difference between "add -A", "add -u", "add .", and "add *"
Corey Schafer
Preparing for a Python Interview: 10 Things You Should Know
Corey Schafer
SQL Tutorial for Beginners 1: Installing PostgreSQL and Creating Your First Database
Corey Schafer
SQL Tutorial for Beginners 2: Creating Your First Table
Corey Schafer
SQL Tutorial for Beginners 3: INSERT - Adding Records to Your Database
Corey Schafer
Linux/Mac Terminal Tutorial: Navigating your Filesystem
Corey Schafer
Python: Ex Machina Easter Egg - Hidden Message within the Code
Corey Schafer
Mac Tip: New Split Screen Feature in El Capitan
Corey Schafer
Setting up a Python Development Environment in Eclipse
Corey Schafer
Git Tutorial: Fixing Common Mistakes and Undoing Bad Commits
Corey Schafer
SQL Tutorial for Beginners 4: SELECT - Retrieving Records from Your Database
Corey Schafer
More on: AI Systems Design
View skill →Related Reads
📰
📰
📰
📰
Why Your React App Freezes Even With Zero API Calls (And How Web Workers Fix It)
Dev.to · ARAFAT AMAN ALIM
React 19 Features — What Actually Changed and What I Use
Dev.to · Safdar Ali
The Share Button Is the Product: Engineering a Viral Loop in Vanilla JS
Dev.to · yunjie
React, Explained Directly — Episode 1: The Fundamentals
Dev.to · surajrkhonde
🎓
Tutor Explanation
DeepCamp AI