[0:00] Today we're going to learn about [0:01] asynchronous programming in Python with [0:03] Async IO as quickly as possible. So let [0:05] us get right into it. [0:11] [Music] [0:15] >> All right, so as always when it comes to [0:17] these tutorials where I try to cover [0:18] something as quickly as possible, this [0:20] is not going to be a deep dive. We're [0:22] not going to go into a lot of details. I [0:24] wouldn't even call it a crash course. [0:25] It's more like me giving you a very very [0:27] quick introduction into the topic and [0:29] then you can continue to study it on [0:31] your own or if you want to you can leave [0:33] me a comment in the comment section down [0:34] below and let me know that you're [0:36] interested in a more detailed course so [0:38] maybe I can do that as well on my [0:39] channel but asynchronous programming [0:42] belongs to the category of concurrent [0:44] programming in Python and there are [0:45] three major ways to do concurrent [0:47] programming in Python one is [0:49] asynchronous programming another one is [0:51] multi-threading and another one is [0:53] multipprocessing for the last two I [0:55] already have two videos on my channel [0:57] similar to this one where I cover them [0:59] as quickly as possible. So you can take [1:00] a look at them if you want to. And today [1:03] we're going to talk about asynchronous [1:04] programming. Now in a nutshell, you want [1:06] to use multipprocessing when you have a [1:08] lot of CPUbound tasks that you want to [1:10] parallelize. So heavy computations that [1:13] you want to do simultaneously on [1:15] multiple CPU cores. You actually want to [1:17] have multiple processes. Uh you don't [1:19] want to be limited by the global [1:20] interpreter log. Multi-threading is a [1:23] little bit more I wouldn't necessarily [1:25] say exotic, but it's a bit odd in Python [1:28] because you have the global interpreter [1:29] lock and you don't have real [1:30] multi-threading unless you release the [1:32] global interpreter lock. But [1:34] essentially, you want to use it when you [1:35] don't want to manually handle the [1:38] control. So you don't want to manually [1:39] switch between the different threads and [1:41] you also maybe have to work with [1:43] something that doesn't support [1:44] asynchronous programming. But I want I [1:46] don't want to talk about this too much. [1:47] I want to focus on asynchronous [1:49] programming today. uh which is the topic [1:51] of this video. So let us go into our [1:53] coding directory. In my case, I'm going [1:55] to navigate to the tutorial directory. [1:57] And here now I'm going to create a file [1:58] called main.py. Now I'm going to start [2:00] by importing async io and creating a [2:03] so-called co- routine. So a co- routine [2:05] is basically a program component that [2:06] can be suspended and resumed. So we can [2:08] pause this. We can continue with this. [2:11] And we define it by saying async defaf [2:13] and then the name IO task. So an [2:15] asynchronous function essentially. In [2:17] this case, this one takes name, delay, [2:19] and number of iterations as a parameter. [2:22] Then we have a couple of iterations in [2:24] this loop here. And we just print the [2:26] task name and the current iteration just [2:28] so we can keep track of what is actually [2:30] happening. And the key thing here is [2:32] awaiting the async io. Call. So this is [2:35] just a placeholder. You could have [2:37] anything here awaiting something that is [2:39] asynchronous. So this could be also [2:41] waiting for a response from a server. [2:43] Basically just any downtime that can be [2:46] used in this single thread that we're [2:47] running. Now asynchronous programming [2:49] runs in a so-called event loop. We have [2:52] one thread so we don't have any [2:53] concurrent I mean we do have concurrency [2:55] but we don't have any parallel [2:57] execution. We don't have multiple [2:58] threads. We don't have multiple [3:00] processes. We have one thread and the [3:02] event loop basically switches between [3:04] the co- routines. So in this case, what [3:06] we're saying here is we're saying print [3:09] a statement and then give back control [3:11] yield back to the event loop and allow [3:14] it to do something else while we're [3:16] doing this. So we're basically saying [3:18] sleep for whatever we pass as delay [3:21] seconds and then go back here. So every [3:24] iteration each iteration here is going [3:26] to call this await async io sleep which [3:29] means we're giving back control to the [3:30] event loop and the event loop can then [3:32] determine which of the other co- [3:34] routines are capable of resuming. So we [3:37] can see that this works by defining an [3:39] asynchronous main function. What we do [3:41] here is we measure the time of the [3:43] executions. We have one time here an [3:45] async io gather call. So we're calling [3:48] the gather function and we're passing [3:51] here three tasks called A, B and C with [3:53] different delays but the same number of [3:55] iterations. And these are going to be [3:58] executed asynchronously. So concurrently [4:00] as three co- routines which basically [4:02] means when A is sleeping we can do B. [4:05] When A and B are sleeping we can do C [4:07] and so on. So we can switch back and [4:09] forth because we have this downtime this [4:11] idle time. Uh in addition to that down [4:14] below here we have three separate await [4:16] statements. So we await three tasks in a [4:18] row. So this is serially. This is not [4:20] concurrently. We're not using the gather [4:22] function. And this basically means task [4:25] A has to be executed. Then task B has to [4:28] be executed. Task C has to be executed. [4:30] And then we're done. Now of course here [4:32] I also need to import time and also of [4:35] course we need to run the main function [4:37] here. We do that by starting an event [4:39] loop by creating an event loop with [4:41] async io run. So we do async io run and [4:44] we pass main. But we don't pass main as [4:47] a function. So as a reference to the [4:49] function, we actually call main and we [4:51] do that in async.io run. So we're [4:53] actually using parenthesis in here. So [4:56] when I run this now, you can see we have [4:58] a, b, and c being executed concurrently. [5:01] So this happens um yeah at the same time [5:04] basically 4.5 seconds. Whereas if I do [5:07] that separately, we can see that we have [5:09] first a then b then c and this is going [5:12] to take much longer 11 seconds. Now, [5:15] this also happens if we're not awaiting. [5:18] Await is the keyword that returns [5:20] control back to the event loop. So, if I [5:22] instead cause some downtime here with [5:24] time. Which is perfectly fine. I can do [5:27] that. If I say time.sleep delay instead [5:30] of async io sleep delay, this doesn't [5:33] work anymore because now I'm never [5:34] returning control back to the event [5:36] loop. I now basically say there is some [5:39] downtime. But since we're not using [5:40] multi-threading here, what is actually [5:42] happening is we're just waiting. we're [5:44] blocking uh the threat. So we're just [5:47] waiting for this to finish before we can [5:49] move on. So if you actually want to give [5:51] control back to the event loop, you have [5:53] to use await. And in this case, you [5:55] would have to use async io. Another [5:58] thing that we can do is we can run tasks [6:00] in the background and we can return [6:02] them, save them into a variable and then [6:04] await them at some point later in the [6:05] function. So here for example, I have [6:07] this background task which prints [6:09] running then waits for 5 seconds then [6:11] prints finishing. And what I can do here [6:13] is I can do async.io.create task with [6:17] background task being called in here. Uh [6:19] this returns then the task instance. [6:22] Whatever happens afterwards is executed [6:24] immediately. So this print statement for [6:26] example. But then I can also await the [6:28] task and I can say okay don't continue [6:31] until this is done and then print the [6:34] final statement. And of course [6:35] everything that happens after async io [6:37] run also has to wait for all of this to [6:40] finish. So if I run this you can see [6:42] continuing immediately um even before [6:44] running is being printed and then only [6:47] when this background task is finished [6:49] because we're awaiting it here only then [6:51] do we get but for this we need to wait [6:53] and this waits two what can also be [6:55] interesting is using the wait function [6:57] in this case here we have again a very [6:59] simple setup we have two co- routines [7:01] print statement waiting time print [7:03] statement return value here with 2 [7:05] seconds here with 5 seconds and then we [7:08] have them as two tasks and Then we use [7:10] the weight function not the gather [7:12] function. This allows us to specify a [7:15] return condition. So in this case we do [7:17] done and pending await async io.we and [7:21] then we specify here return when async [7:23] io first completed. What this basically [7:25] means is that this whole thing is going [7:28] to return. So we're going to stop [7:30] waiting when one of them returns. So [7:32] when the first one returns we're going [7:34] to continue with the code and this is [7:36] going to return two things. It's going [7:37] to return the list of the tasks that are [7:39] finished. So that are done and the list [7:42] of the tasks that are not finished yet. [7:44] So we can also print all of that after [7:46] this is being awaited. We can print the [7:48] finished tasks and the pending tasks. [7:50] And then in the end we can also do [7:52] another await asai await pending to wait [7:55] for the remaining tasks. So let's run [7:57] this now. You can see one start two [8:00] start then one finishes one end. You can [8:02] see the finished tasks are uh result is [8:05] equal to one done and then we have the [8:07] pending tasks which doesn't have a [8:10] result yet. So we have a future and at [8:12] some point then this also finishes and [8:14] we get to end. Now if you don't want to [8:17] wait indefinitely, you can also use the [8:18] wait for function. This allows us to [8:20] specify a timeout. In this case we have [8:22] a long operation taking 5 seconds and we [8:25] only allow for 2 seconds by using wait [8:27] four. In the case that these two seconds [8:29] are surpassed we get a timeout error. we [8:32] can catch that and handle it. Uh but in [8:34] this case, we're just going to print [8:36] took too long. So if I run this, you're [8:37] going to see one, two, took too long [8:40] because this takes 5 seconds and this [8:43] takes 2 seconds. Of course, this only [8:45] works with a wait because we need to [8:46] pass control back to the event loop. Uh [8:48] it doesn't work if I use time. Because [8:50] then it's going to block the threat. And [8:52] that's basically it. There's of course [8:54] much more to cover. Manual stuff you can [8:56] do with the event loop, task groups, [8:58] shielding, and so on. There's much more [8:59] to cover in general when it comes to [9:01] concurrency in Python. If you want to [9:03] have more detailed tutorials, let me [9:04] know in the comment section down below. [9:06] So, that's it for today's video. I hope [9:08] you enjoyed it and hope you learned [9:09] something. If so, let me know by hitting [9:11] a like button and leaving a comment in [9:12] the comment section down below. Also, [9:14] don't forget to check out the similar [9:15] videos I already have on multi-threading [9:17] and multipprocessing. And of course, [9:19] don't forget to subscribe to this [9:20] channel and hit the notification bell to [9:22] not miss a single future video for free. [9:24] Other than that, thank you much for [9:25] watching. See you in the next video and [9:27] bye.