imagine programming is a journey from point A to D in traditional synchronous programming we travel in a straight line stopping at each point before moving to the next this means if there's a delay at any point everything pauses until we can move on now a synchronous programming changes the game it allows us to start tasks at b c and d even if the task at a isn't finished yet this is like sending out Scouts to explore multiple paths at once without waiting for the first Scout to return before sending out the next this way our program can handle multiple tasks simultaneously making it more efficient especially when dealing with operations that have waiting times like loading a web page and that's the essence of asynchronous programming making our code more efficient by doing multiple things at once without the unnecessary waiting so now let's quickly discuss when we should use async iio because when we build software choosing the right concurrency model and picking between asyn iio threads or processes is crucial for performance and efficiency now async iio is your choice for tasks that wait a lot like Network requests or reading files it excels in handling many tasks concurrently without using much CPU power this makes your application more efficient and responsive when you're waiting on a lot of different tasks now threads are suited for tasks that may need to wait but also share data they can run in parallel within the same application making them useful for tasks that are IO bound but less CPU intensive IO meaning input output now for CPU heavy tasks processes are the way to go each process operates independently maximizing CPU usage by running in parallel across multiple cores this is ideal for intensive computations in summary choose asyn iio for managing many waiting tasks efficiently threads for parallel tasks that share data with minimal CPU use and processes for maximizing performance on CPU intensive tasks now that we know when to use async iio let's dive into the five key Concepts that we need to understand the first concept is the event Loop in Python's async iio The Event Loop is the core that manages and distributes tasks think of it as a central Hub with tasks circling around it waiting for their turn to be executed each task takes its turn in the center where it's either executed immediately or paused if it's waiting for something like data from the internet when a task awaits it steps aside making room for another task to run ensuring the loop is always efficiently utilized once the awaited operation is complete the task will resume ensuring a smooth and responsive program flow and that's how async io's event Loop keeps your Python program running efficiently handling multiple tasks a synchronously so just a quick pause here for any of you that are serious about becoming software developers if you want to be like Max who landed a 70k per your job in Just 4 months of work consider checking out my program with course careers now this teaches you the fun fundamentals of programming but also lets you pick a specialization taught by an industry expert in front end backend or devops beyond that we even help you prepare your resume we give you tips to optimize your LinkedIn profile how to prepare for interviews we really only succeed if our students actually get jobs that's the entire goal of the program so if that's at all of interest to you we do have a free introduction course that has a ton of value no obligation no strings attached you can check it out for free from the link in the description so now that we understand understand what the event Loop is it's time to look at how we create one and then talk about the next important concept which is co- routines now whenever we start writing asynchronous code in Python We Begin by importing the async io module now this is built into python you don't need to install it and for the purpose of this video I'll be referencing all of the features in Python version 3.11 and above so if you're using an older version of python just make sure you update it because some things have changed in the recent versions so we begin by the module then we use the command or the line async i.run and we pass to this something known as a co-routine function which will return a co- routine object now asyn i.run is going to start our event Loop and it's going to start that by running a co- routine now in our case there's two types of co- routines we're concerned with we have a co- routine function which is this right here and we have what's returned when you call a co- routine function I know it seems a bit strange but when you call Main like this when it's defined using this async keyword this returns to us something known as a co- routine object now the co-routine object is what we need to pass here to async i.run it's going to wait for that to finish and it's going to start the event Loop for us where it handles all of our asynchronous programming so recap import the module Define some asynchronous functions so async and then you write the function name out this is known as a co- routine function you then call the function and pass that to async i.run and that's
going to start your event Loop and allow you to start running asynchronous code that starts from this entry point now to illustrate this a bit further let's look at the difference between an asynchronous function something defined with this async keyword and a normal function so watch what happens if I go here and I simply call this function some of you may assume that it's simply going to print out start of main Cod routine but you'll see that that's actually not the case I know that my terminal is a little bit messy here but it says co-routine main was never awaited now the reason we get that issue is because when we call the function here what we're actually doing is we're generating a co-routine object this co-routine object needs to be awaited in order for us to actually get the result of its execution now if we want to see this even more visually we can actually print out what we get when we call this main function so let's call it here and notice that we actually get this co-routine object so when you call a function defined with the async keyword it returns a co-routine object and that coroutine object needs to be awaited in order for it to actually execute so that's why we use the async i.run syntax because this will handle awaiting this Co routine and then allow us to write some more asynchronous code now the next thing that we need to look at is the await keyword now the await keyword is what we can use to await a coverou tetine and to actually allow it to execute and for us to get the result the thing is though we can only use this awake keyword inside of an asynchronous function or inside of a code routine so let's write another code routine and see how we would await it and how we get its result so now I've included a slightly more complex example where we're actually waiting on a different code routine just to see how that works so notice that we have a code routine up here and what this is aiming to do is simulate some input output bound operation now that could be going to the network and retrieving some data trying to read file something that's not controlled by our program that we're going to wait on the result from so in this case you can see that we fetch some data we delay so we're just sleeping for a certain amount of seconds just to simulate that input output bound operation we then get the data and we return it now we know this is a co- routine because we've defined it as an asynchronous function now remember that in order for a co- routine to actually be executed it needs to be awaited now in this case what we do is we create a task and this task is the co-routine object now the co-routine object at this point in time is not yet being executed and the reason it's not being executed yet is because it hasn't been awaited what I'm trying to show you is that when you call an asynchronous function it returns a co- routine that co- routine needs to be awaited before it will actually start executing so in this case here we now await the task when we await it it will start executing and we'll wait for it to finish before we move on to the rest of the code in our program so let's run the code and see what the output is here and you can see it says start of main code routine data fetched it then receives the results and it says the end of the main code routine now let's clear that and let's look at a slightly different example so let's take this result code right here and let me just get rid of this comment and let's put this actually at the end of this function so now what we have is print start of main co- routine we create the co- routine object we then print end of main routine object then we await the code routine and I just want to show you the difference in the result that we're going to get so let's run the code and notice we get start of main code routine end of main code routine and then we get fetching data data fetched and then we get the result now the reason we got this is because we only created the code routine object here we didn't yet await it so it wasn't until we hit this line right here that we waited for the execution of this to finish before moving on to the next line it's really important to understand that fact that a code routine doesn't start executing until it's awaited or until we wrap it in something like a task which we're going to look at later so I've made a slight variation to the last example and you can see what we're doing now is we're creating two different code routine objects and we're then awaiting them now I want you to pause the video and take a guess of what you think the output's going to be and how long you think it will take for this to execute go ahead pause the video I'm going to run the code now and explain what happens so when I run this if if we move it up here you'll see that we get fetching data id1 data fetched id1 we then receive the result and then we go ahead and we fetch it for id2 now let's clear this and run it one more time and you can see that it takes 2 seconds we fetch the first result it takes another 2 seconds and we fetch the second result now this might seem counterintuitive because you may have guessed that when we created these two coroutine objects they were going to start running concurrently and that means that it would only take us a total of 2 seconds and we'd immediately get both of the results but remember a code routine doesn't start running until it's awaited so in this case we actually wait for the first co- routine to finish and only once this has finished do we even start executing the second co- routine meaning that we haven't really got any performance benefit here we've just created a way to kind of wait for a task to be finished that's all we've really learned at this point in time now that we understand this concept we can move over and talk about tasks and see how we can actually speed up an operation ation like this and run both of these tasks or these co- routines at the same time so now we're moving on to the next important concept which is a task now a task is a way to schedule a co- routine to run as soon as possible and to allow us to run multiple co- routines simultaneously now the issue we saw previously is that we needed to wait for one co- routine to finish before we could start executing the next with a task we don't have that issue and as soon as a co- routine is sleeping or it's waiting on something that's not in control of our program we can move on and start executing another task we're never going to be executing these tasks at the exact same time we're not using multiple CPU cores but if one task isn't doing something if it's idle if it's blocked if it's waiting on something we can switch over and start working on another task the whole goal here is that our program is optimizing its efficiency so we're always attempting to do something and when we're waiting on something that's not in control of our program we switch over to another task and start working on that so here's a quick example that shows you how we would optimize kind of the previous example that we looked at what we do here is we use the simple create task function now there's a few other ways to make tasks which I'm going to show you in a second but this is the simplest what we do is we say task one is equal to asyn io. create task and then we pass in here a co-routine object it's a co-routine object because this is a co-routine function we call the function and that returns to us a co- routine so in this case we pass an ID then we pass some time delay now if this was running synchronously so if we had to wait for each of these tasks to run it would take us 2 seconds plus 3 seconds plus 1 second so a total of 6 seconds for this code to execute however you'll see now that what will happen is we'll be able to execute this code in simply 3 seconds because as soon as one of the tasks is idle and we're waiting on this sleep we can go and execute or start another task now what I do is I still need to await these tasks to finish so I just await them all in line here and then collect all of their different results so let's bring the terminal up and let's run this code and make sure it works and notice that it starts all three Co routines pretty much immediately and then we get all of the data back at once in about 3 seconds again that differs from if we were to use just the normal C routines and we didn't create a task we'd have to wait for each of them to finish before we can move on to the next one so as a quick recap when we create a task we're essentially scheduling a code routine to run as quickly as possible possible and we're allowing multiple Co routines to run at the same time as soon as one co- routine isn't doing something and it's waiting on some operation we can switch to another one and start executing that now all of that is handled by the event loop it's not something we need to manually take care of however if we do want to wait on one task to finish before moving to the next one we can use the await syntax so it would be possible for me to go here and write some code like this and now we would see if we execute the code and we can go ahead and do that that we'll start the first and the second code routine but we won't start the third one until the first and the second one are done so using a synchronous programming gives us that control and allows us to synchronize our code in whatever manner we see fit so now we move on to a quick example where I'm going to show you something known as The Gather function Now The Gather function is a quick way to concurrently run multiple co- routines just like we did manually before so rather than creating a task for every single one of the co- routines using that create task function we can simply use gather and it will automatically run these concurrently for us and collect the results in a list the way it works is that we pass multiple code routines in here as arguments these are automatically going to be scheduled to run concurrently so we don't need to wait for them to finish before we start executing the next one and then we will gather all of the results in a list in the order in which we provided the co- routines so the result of this one will be the first element in the list second element in the list third element in the list Etc and it's going to wait for all of them to finish when we use this await keyword which just simplifies this process for us that then allows us to have all of the results in one place so we can parse through them using this for Loop so let's go ahead and run this code and you see that it starts all three of our Co routines we wait 3 seconds and then we get all of our different results now one thing you should know about gather is that it's not that great at error handling and it's not going to automatically cancel other co- routines if one of them were to fail now the reason I'm bringing that up is because the next example I show you does actually provide some built-in error handling which means it's typically preferred over gather but it's just worth noting that if there is an error that occurs in one of these different code routines it won't cancel the other code routines which means you could get some weird state in your application if you're not manually handling the different exceptions and errors that could occur so now we're moving on to the last example in the topic of tasks where we're talking about something relatively new known as a task group now this is a slightly more preferred way to actually create multiple tasks and to organize them together and the reason for this is this provides some built-in error handling and if any of the tasks inside of our task groups were to fail it will automatically cancel all of the other tasks which is typically preferable when we are dealing with some Advanced errors or some larger applications where we want to be a bit more robust now the fetch data function has not changed at all all we've done here is we've started using async i.ask
group now notice that what I'm using here is the async width now this is what's known as an asynchronous context manager you don't to understand that you don't have to have seen context managers before but what this does is give us access to this TG variable so we create a task group as TG and now to create a task we can say TG our task group. create task just like we did before in that first example we can create an individual task we can then add this to something like our tasks list if we care about the result of it and now once we get by this asynchronous width so once we get down here to where I have the comment what happens is all of these tasks will have already been executed so the idea is this is a little bit cleaner it's automatically going to execute all of the tasks that we add inside of the task group once all of those tasks have finished then this will stop blocking when I say stop blocking that means we can move down to the next line of code and at this point we can retrieve all of the different results from our tasks now there's various different ways to go about writing this type of code but the idea is you simply create a task here as soon as it's created inside of the task group we now need to wait for that and all the other tasks to finish before we unblock from this block of code then once they're all finished we move on to the next lines of code now similarly to any other task that we looked at before these are all going to run concurrently meaning if one task is sleeping we can go on and we can start another task and work on something else so those are tasks obviously there's a lot more you can do here but understand that you run tasks when you want to execute code concurrently and you want multiple different operations to be happening at the same time so now we're moving on to the fourth important concept which is a future now it's worth noting that a future is not something that you're expected to write on your own it's typically utilized in lower level libraries but it's good to just be familiar with the concept in case you see it in asynchronous programming so I'll go through this fairly quickly but really what a future is is a promise of a future result so all it's saying is that a result is to come in the future you don't know exactly when that's going to be that's all future is so in this case you can see that we actually create a future and we await its value what we do is we actually get the event Loop you don't need to do this you'll probably never write this type of code we create our own future we then have a new task that we create using async iio and you can see the task is set future result inside here we wait for 2 seconds so this is some blocking operation and then we set the result of the future and we print out the result here we AIT the future and then we print the result now notice we didn't actually await the task to finish we awaited the future object so inside of the task we set the value of the future and we awaited that which means as soon as we get the value of the future this task may or may not actually be complete so this is slightly different than using a task when we use a future we're just waiting for some value to be available we're not waiting for an entire task or an entire co- routine to finish that's all I really want to show you here I don't want to get into too many details that's a future really just a promise of an eventual result so now we're moving on and talking about synchronization Primitives now these are tools that allow us to synchronize the execution of various co- routines especially when we have larger more complicated programs now let's look at this example so we can understand how we use the first synchronization tool which is lock let's say that we have some shared resource maybe this is a database maybe it's a table maybe it's a file doesn't matter what it is but the idea is that it might take a fair amount of time for us to actually modify or do some operation on this shared resource and we want to make sure that no two co-routines are working on this at the same time the reason for that is if two co-routines were say modifying the same file if they're writing something to the database we could get some kind of error where we get a mutated state or just weird results end up occurring because we have kind of different operations happening at different times and they're simultaneously occurring when we want really wait for one entire operation to finish before the next one completes that might seem a little bit confusing but the idea is we have something and we want to lock it off and only be using it from one co- routine at a time so what we can do for that is we can create a lock now when we create a lock we have the ability to acquire the lock and we do that with this code right here which is async with lock now this again is an asynchronous context manager and what this will do is it will check if any other code routine is currently using the lock if it is it's going to wait until that code routine is finished if it's not it's going to go into this block of code now the idea is whatever we put inside of this context manager needs to finish executing before the lock will be released which means we can do some critical part of modification we can have some kind of code occurring in here that we know will happen all at once before we move on to a different task or to a different code routine the reason that's important is because we have something like an await maybe we're waiting a network operation to save something else that could trigger a different task to start running in this case we're saying hey within this lock wait for all of this to finish before we release the lock which means that even though another task could potentially be executing when the Sleep occurs it can't start executing this critical part of code until all of this is finished and the lock is released so all the lock is really doing is it's synchronizing our different co- routines so that they can't be using this block of code or executing this block of code while another code routine is executing it that's all it's doing it's locking off access to in this case a critical resource that we only want to be accessed one at a time so in this case you can see that we create five different instances of this Co routine we then are accessing the lock and then again once we get down here we're going to release it so if we bring up the terminal here and we start executing this you'll see that we have resource before modification resource after before after before after and the idea is even though that we've executed these cortines concurrently we're gating them off and we're locking their access to this resers so that only one can be accessing it at a time moving on the next synchronization primitive to cover is known as the semaphore now a semaphore is something that works very similarly to a lock however it allows multiple Cod routines to have access to the same object at the same time but we can decide how many we want that to be so in this case we create a semaphore and we give give it a limit of two that means only two co- routine story can access some resource at the exact same time and the reason we would do that is to make sure that we kind of throttle our program and we don't overload some kind of resource so it's possible that we're going to send a bunch of different network requests we can do a few of them at the same time but we can't do maybe a thousand or 10,000 at the same time so in that case we would create a semaphor we'd say okay our limit is maybe five at a time and this way now we have the event Loop automatically handled this throttle our code intentionally to only send maximum five requests at a time anyways let's bring up our terminal here and run this code so Python 3 semap 4. piy and you can see that we can access the resource kind of two at a time and modify it but we can't have any more than that now moving on to the last primitive we're going to talk about this is the event now the event is something that's a little bit more basic and allows us to do some simpler synchronization in this case we can create an event and what we can do is we can await the event to be set and we can set the event and this acts as a simple Boolean flag and it allows us to block other areas of our code until we've set this flag to be true so it's really just like setting a variable to true or false in this case it's just doing it in the asynchronous way so you can see we have some Setter function maybe it takes two seconds to be able to set some result we then set the result and as soon as that result has been set we can come up here we await that so we wait for this to finish and and then we can go ahead and print the event has been set continue execution so we can bring this up here and quickly have a look at this so Python 3 if we spell that correctly event. pi and you'll see it says awaiting the event to be set event has been set event has been set continuing the execution okay pretty straightforward it's just a Boolean flag that allows us to wait at certain points in our program there's lots of different times when you would want to use this but I just wanted to quickly show you that we do have something like this that exists now there is another type of primitive here that's a bit more complicated called the condition I'm not going to get into that in this video in fact I'm going to leave the video here if you guys enjoyed this make sure you leave a like subscribe to the channel and consider checking out my premium software development course with course careers if you enjoy this teaching style and you're serious about becoming a developer anyways I will see you guys in another YouTube [Music] video
2024-04-10