 Hi, I'm Kamal, and I'm going to be giving you a little talk about async.io, which is the asynchronous library that was introduced, I believe, in Python 3.4. And we're going to be looking more at the guts of it than at how to use it. This came out of me realizing a couple of weeks ago that, like, I actually don't really know what an event loop is and how that works. And then a couple of days later, I was in a Skype call with a friend who's in Cincinnati or somewhere, not in Minneapolis, St. Paul's, and we were in Skype, and we're looking at Python async.io source code for two hours in the evening, because that's how things happen sometimes. So raise your hands if you know how the async.io module works in Python 3.4. And raise your hand if you've used async.io in Python 3.anything. Okay, that's like four. I put my hand down, because I don't know where my hand is, but I have actually never written a line of Python 3 in my life, so I've never used async.io. But I do know how it works. I wanted to ask and see, because I thought this was a funny tweet by Jacob Kaplan a few days ago, which is at a conference, who knows how async.io works, and nobody does. How many people have used it? And then a bunch of people. Yeah, but I'm not one of those people who use it. Just one of those people who kind of knows how it works. So in the documentation, this is how async.io is described. This module provides infrastructure for writing single-threaded, concurrent code using coroutines. So the key thing here is concurrent, meaning multiple things can be happening. But single-threaded, we're not actually using the system's version of concurrency, which is threads. We're using just a single thread and somehow managing all of that inside of our Python process. And in Python 3.4, you can write a little coroutine like this, and a coroutine is just a generator that has a decorator on top of it. As far as I can tell, that's all a coroutine is. So here's an example. It prints high. And then yield, which is the same yield we know from generators, which kind of pops out of the generator for a little bit. Here is the way we think about it mentally as we're yielding control to some other coroutine to run. And then when it's our turn again, we'll print, I'm done, bye. And then that's a very, very simple example. Here's another example where we're using something slightly more interesting than just yielding control for no reason. We're sleeping. And so we print, I'm sleepy. And then yield, and we don't use the, I'm going to say time.sleep or thread.sleep. I never remember where that is. We use async.sleep, which is a version of sleep that's designed to work with async.io. And then when we get control back, we'll print much more refresh now. And one last example that involves some I.O. We print that we want some data. And then we yield from, oh yeah, I forgot to say yield from familiarity. Yes, no, maybe, okay. I forgot to put a couple of examples in to explain what yield from is, but if you've ever had a generator and you want to yield everything from that generator to another function, it's basically the same as doing for I in async.io.sleep, yield I. In this case, async.io.sleep just yields once. It's not super important, but it is super important when using this to actually write yield from. Otherwise your async stuff will be a mess. I won't get into that right now, but I want to now get down to the guts a little bit. So we say that it's single threaded but concurrent. So how does that work? Most of the time in Python when we're doing concurrency, unless you're using twisted or one of the other things that async.io kind of is similar to, you want concurrency, you're going to have multiple threads. You want to handle multiple connections at the same time, multiple threads. With async.io you can do that with a single thread. But how? It uses an event loop. An event loop is what, to me, two or three weeks ago was a very scary word, and I was just like, yeah, something, events, something, sockets, I know JavaScript has an event loop. But then after looking at the source code for a couple of hours, it's like, well, actually it's not that scary. The event loop works with callbacks, and a callback is just a function. You give a callback a function to the event loop for it to call at some time, and it will call it for you later. So it doesn't directly work with coroutines, and we'll come back to that in a little bit. Like I said, this is more about the guts. So we're going to just look at the event loop in terms of callbacks. So the event loop is really basically this. It's just a big while loop, does something for a very long time, and the thing it does is collect all of the callbacks that are ready to be called now, and then runs them all. And then it does that again. Collect all the ones that are ready to be run now, and then runs them all. And that just goes on forever. How do we ever get callbacks into the event loop? So there are three main ways to get callbacks into the event loop. One is to call loop.callsoon, and then passing in a callback. And then that callback is considered ready immediately. So whenever we're in that loop and we're like, get all the ready ones, that callback is ready right away, ready to go. Another way is to call with a delay or call at a specific time. So loop.callat with the time to call it at in the callback or call after a delay. And in these two cases, the callback is considered ready after that time has passed or once the delay has elapsed. And finally, there's a couple of ways to add callbacks that get run when IO stuff has happened. So add reader with a file descriptor, which is typically a socket. And a callback is like, whenever there's data ready to be read at this file descriptor, call my callback. And writer is, whenever it's writable, whenever it's ready to have data written to it, call my callback. And the reason that you have to check if, so yeah, I'm sorry, in ordinary thread-based concurrency, when you write to a socket, the write would block and your thread kind of takes a rest until the OS has dispatched whatever data you've written and then brings you back to life. It's like, OK, your data's gone now. Continue doing whatever you want it to do. This is sort of analogous to that. Instead of relying on the kernel's way of handling readiness for data, we're bringing that into Python into this event loop. So I said that I'd get back a little bit to how coroutines get translated into callbacks because a coroutine is a generator with an app on it. But we said that the event loop only works with callbacks, which are functions. So how do we get from a coroutine to a callback? And so there's this class in AsyncOut called a task. And all it is is a little wrapper around a coroutine. And so you create a task by passing it a coroutine. And then it provides a method called step. And what step does is pokes the generator. So if you remember, generator is something that if you call next on it, it will give you a value. But then it will just hang around until you call next on it again. It doesn't return a list. It just returns a thing one at a time. So the way that this poking happens is the task calls the coroutines next or send is another part of the generator protocol. But basically, we can just imagine that it's calling next on the coroutine. And so the task is just this thing that surrounds the coroutine and provides the step method. And now with the step method, it will be available to us as a callback. Task.step is now just a function. It handles the stuff inside to make the generator do things. But it's now just a function. If we call task.step, it will cause one iteration of whatever the underlying generator is. And task.step is now a thing we can pass in to add reader or call su and or call later. And here's a little digression into how the IO parts of this all work out, which is there's an abstraction in the standard library called the selector, which allows checking for events, meaning readable or writable, on multiple sockets at once. And this is the key part about how we bring the scheduling around IO into the Python process instead of leaving it in the hands of the OS. And the selector provides very, very, very simple interface. One is you can register a file descriptor. Events is ready for reading or ready for writing. And then a key is just a piece of data that will come back to you. So the second part of the API is select with a timeout. And this is OK operating system. I've added a bunch of different sockets that I'm interested in. As soon as one of them or any of them are ready, let me know. And this will give you back a list of all of the keys that you put in with the register method. So there's a way to tell the operating system you're interested in a bunch of sockets. And this gets used in the event loop. So this hand-drawn courier wasn't going to work for the entire event loop. So I kind of punted and went over here. I know. What? Really? OK. I can make the triangle really big. I'm trying to zoom in. Anyway, when I said the event loop is really not scary, this is the guts of the AsyncIO event loop. Details for exception handling are removed. Details for cancellation are removed because it lets you cancel tasks. But otherwise, you can read it and all of the parts I'm showing here are what shows up. So to create a new event loop, there are three parts. We have, if you remember, call soon, make something ready immediately. So we have a double-ended queue for callbacks that are ready. We have an initially empty list of scheduled callbacks, which are ones that were added with call later or call at. And we've got a selector, which is this thing that lets us ask the operating system to register multiple file descriptors with it and then tell us about when they're ready. So call soon, that's it. You do call soon, you just append to the ready list. When I said it's ready immediately, I meant it's just put in the list of things that are ready. And when you do call at or call after, it maintains the scheduled list as a priority queue. So that's what this heap queue, heap pushes. It's just so that whenever we get the thing off the front, it will always be the one with the earliest time to be called at. That way, we don't have to look through for the next callback to run, it's just always at the front. So call at is, we insert the time and the callback into the scheduled list. And call after is just like, often you don't want to call something at 3.03 AM in 27 seconds. You want to call it in two seconds, but it happens to be 3.03 AM in 25 seconds. So this call after is just a little helper for that. Again, super, super simple. Add reader and add writer. Both just use the register functions from the selector that we had. And so the reader says, hey, this file descriptor, tell me about whenever it's ready to be read from. And give me this callback when that happens. And add writer does the same except with when it's ready for writing. And so this is the body of the event loop now. This is one run of the event loop. So the event loop was collect all the ready events and then run all the ready events. So collecting all the ready events. First, we find out how long until the first event that's already ready or that was scheduled. So if something was already ready, so there was something in the ready list, then we don't want to wait at all because we have something ready already. God. And otherwise, if there's something in the scheduled list, then we want to know how long do we have to work with until that thing's time is ready to come. So this is just figuring out how long we have to wait. And then we go into that selector and say to the OS, hey, I'm going to wait for three and a half seconds. Tell me if any of these file descriptors that I gave you before are ready in that time. And then it will give you back the callbacks of any that became ready in that time. So we just add all of those callbacks to ready. We're just extending the ready list with more things that are ready to go. And then getting all of the scheduled callbacks whose timers have expired, we just go through the scheduled list. And again, this priority queue business, we use heap queue pop left. It takes off the front of the scheduled list. And we do that until the front of the list is in the future. So we only want to collect all of the callbacks whose timeouts have passed in the past. All right. So now we had callbacks that were initially ready. We added all the callbacks that came out of the IO, the selector. And now we have also added all the ones that come out of the scheduled callbacks. So now we just run all the callbacks. And it looks like this. You see how many callbacks there were. Then you go through each one and get the callback out of the list and call the callback. And there's a little subtlety here why we don't just do for callback in self.ready. Why we actually take the length first and loop is because in running those callbacks, we may be adding more callbacks to the ready list. One of these callbacks might call call soon. So if that happened, we would never finish this loop. We'd just keep looping forever. So we only take the ones that are ready right now. And any other ones that get added on have to wait their next turn for the next stick of the loop. And then this is the last little bit. The event loop in async.io basically is wildtrue self.run once. That's pretty much it. I do recommend reading this code. It's actually surprising how readable minus the exception handling and cancellation stuff is really clear. And I haven't yet, but I intend to go through another event loop maybe twisted. And I think that having read this one that's pure Python is super great for understanding whichever event loop I choose to tackle next. But yeah, I heartily recommend it. Thank you.