Welcome to an Asyncio with Python tutorial. This tutorial will be specifically for Python 3.5+, using the latest asyncio keywords. Asyncio is the standard library package with Python that aims to help you write asynchronous code by giving you an easy way to write, execute, and structure your coroutines. The Asyncio library is for concurrency, which is not to be confused with parallelism.
Let's say you're out for a jog, feel some tapping on your shoe, you glance down, and notice your shoe laces are untied! Do you have to finish your run before you can tie your shoe? Of course not, you can stop, or suspend, your run for a moment while you tie your shoe, then resume running again. While you run, you might monitor your shoe laces, and if they were to become untied again, you could stop again. Just like you probably don't finish your entire run before you re-tie your shoes, you also probably didn't tie your shoe while running, this would be quite hard. I am not saying it's impossible, but it would be hard. Because of this, you're actually running and keeping your shoes tied concurrently. You might be handling multiple tasks at once, but you're not really doing multiple tasks at once, which would be in parallel.
The main pain-points that asynchronous programming aim to solve are to allow for situations where your shoe needs to be tied, but you really don't want to wait until you're done with your jog. In fact, it would be dangerous! The same is true for a web page. Let's say you're loading resources from various locations, and one of the images hosted on some server is just not responding. With synchronous loading, this is going to hang your entire page. With asynchronous loading, other elements of your page can load while you wait for the response from the other server.
For the purpose of this tutorial, we just need some sort of function(s) that take variable amounts of time that we can use to illustrate both he problem and solution. For this, I am going to create a function that will iterate through a range that we pass via a parameter, and then the function finds all of the values in that range that are divisible by the 2nd parameter that we pass:
def find_divisibles(inrange, div_by): print("finding nums in range {} divisible by {}".format(inrange, div_by)) located = [] for i in range(inrange): if i % div_by == 0: located.append(i) print("Done w/ nums in range {} divisible by {}".format(inrange, div_by)) return located
Next, let's add a main()
function, which will serve as our main block of code that executes these functions:
def main(): divs1 = find_divisibles(508000, 34113) divs2 = find_divisibles(100052, 3210) divs3 = find_divisibles(500, 3)
Finally, let's just run the main function:
if __name__ == '__main__': main()
Full script up to this point:
def find_divisibles(inrange, div_by): print("finding nums in range {} divisible by {}".format(inrange, div_by)) located = [] for i in range(inrange): if i % div_by == 0: located.append(i) print("Done w/ nums in range {} divisible by {}".format(inrange, div_by)) return located def main(): divs1 = find_divisibles(508000, 34113) divs2 = find_divisibles(100052, 3210) divs3 = find_divisibles(500, 3) if __name__ == '__main__': main()
Running this, we can see the first one hangs us up for a bit (finding nums in range 50800012 divisible by 3411321
), then we get our answer, then the other two are fairly fast. The full output:
finding nums in range 508000 divisible by 34113 Done w/ nums in range 508000 divisible by 34113 finding nums in range 100052 divisible by 3210 Done w/ nums in range 100052 divisible by 3210 finding nums in range 500 divisible by 3 Done w/ nums in range 500 divisible by 3
As we can see, each function was run in the order that we called it, and we had to wait for the first long one to complete for the other 2, shorter ones, could complete.
It's not really necessary in this case that we run like this, what if we'd like the other 2 to return if possible? Enter Asyncio
!
We know what Asynchronous programming means now. Let's get acquainted with some of the terms and how we're using them. Asyncio is a fairly large library, be sure to check out other tutorials and the asyncio documentation to get ideas for how you can make more use of Asyncio.
In order to use Asyncio, we will need an event loop
, a way input tasks to be done in this loop, and of course the tasks themselves! With asynchronous programming, you're going to want your tasks to be coroutines. Under the current syntax with Python 3.5 onward, the word "coroutine" isn't actually used. Instead, to denote your coroutines, you will preface your function with async
. Rather than def foo()
you will have async def foo()
.
We can make that change now:
async def find_divisibles(inrange, div_by): print("finding nums in range {} divisible by {}".format(inrange, div_by)) located = [] for i in range(inrange): if i % div_by == 0: located.append(i) print("Done w/ nums in range {} divisible by {}".format(inrange, div_by)) return located async def main(): divs1 = find_divisibles(508000, 34113) divs2 = find_divisibles(100052, 3210) divs3 = find_divisibles(500, 3) if __name__ == '__main__': main()
Let's also:
import asyncio
Next, we need our event loop. Let's add that:
if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
Next, we need to actually create tasks, making our main now:
async def main(): divs1 = loop.create_task(find_divisibles(508000, 34113)) divs2 = loop.create_task(find_divisibles(100052, 3210)) divs3 = loop.create_task(find_divisibles(500, 3))
Now, we want to want to wait for these tasks to complete. If we don't wait for these to complete, they'll start, the loop will end, close, and the program will exit before we get our results.
async def main(): divs1 = loop.create_task(find_divisibles(508000, 34113)) divs2 = loop.create_task(find_divisibles(100052, 3210)) divs3 = loop.create_task(find_divisibles(500, 3)) await asyncio.wait([divs1, divs2, divs3])
Our full script now:
import asyncio async def find_divisibles(inrange, div_by): print("finding nums in range {} divisible by {}".format(inrange, div_by)) located = [] for i in range(inrange): if i % div_by == 0: located.append(i) print("Done w/ nums in range {} divisible by {}".format(inrange, div_by)) return located async def main(): divs1 = loop.create_task(find_divisibles(508000, 34113)) divs2 = loop.create_task(find_divisibles(100052, 3210)) divs3 = loop.create_task(find_divisibles(500, 3)) await asyncio.wait([divs1, divs2, divs3]) if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
Running this, we get
finding nums in range 508000 divisible by 34113 Done w/ nums in range 508000 divisible by 34113 finding nums in range 100052 divisible by 3210 Done w/ nums in range 100052 divisible by 3210 finding nums in range 500 divisible by 3 Done w/ nums in range 500 divisible by 3
It doesn't appear that we've done things asynchronously yet, what gives? Well, our find_divisibles
coroutine doesn't actually ever suspend itself, so it still runs all the way through. In an effort to keep things simple, let's suspend it with a quick, conditional sleep:
if i % 500000 == 0: await asyncio.sleep(0.0001)
Making the full function:
async def find_divisibles(inrange, div_by): print("finding nums in range {} divisible by {}".format(inrange, div_by)) located = [] for i in range(inrange): if i % div_by == 0: located.append(i) if i % 500000 == 0: await asyncio.sleep(0.0001) print("Done w/ nums in range {} divisible by {}".format(inrange, div_by)) return located
Now our full script:
import asyncio async def find_divisibles(inrange, div_by): print("finding nums in range {} divisible by {}".format(inrange, div_by)) located = [] for i in range(inrange): if i % div_by == 0: located.append(i) if i % 500000 == 0: await asyncio.sleep(0.0001) print("Done w/ nums in range {} divisible by {}".format(inrange, div_by)) return located async def main(): divs1 = loop.create_task(find_divisibles(508000, 34113)) divs2 = loop.create_task(find_divisibles(100052, 3210)) divs3 = loop.create_task(find_divisibles(500, 3)) await asyncio.wait([divs1, divs2, divs3]) if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close()
Running this, I get:
finding nums in range 508000 divisible by 34113 finding nums in range 100052 divisible by 3210 finding nums in range 500 divisible by 3 Done w/ nums in range 100052 divisible by 3210 Done w/ nums in range 500 divisible by 3 Done w/ nums in range 508000 divisible by 34113
As you can see, the solutions are out of order, where the largest task finishes last, the others before it.
What if you wanted the results of these functions? You can just return the divs in the main loop:
async def main(): divs1 = loop.create_task(find_divisibles(508000, 34113)) divs2 = loop.create_task(find_divisibles(100052, 3210)) divs3 = loop.create_task(find_divisibles(500, 3)) await asyncio.wait([divs1, divs2, divs3]) return divs1, divs2, divs3
We can collect these from where we run the loop:
d1, d2, d3 = loop.run_until_complete(main())
Which goes into:
if __name__ == '__main__': loop = asyncio.get_event_loop() d1, d2, d3 = loop.run_until_complete(main()) loop.close()
At the moment, d1
for example is an asyncio task. We can fetch the result of it, however, with .result
:
if __name__ == '__main__': loop = asyncio.get_event_loop() d1, d2, d3 = loop.run_until_complete(main()) # Access us! print(d1.result()) loop.close()
Running this gives us:
finding nums in range 508000 divisible by 34113 finding nums in range 100052 divisible by 3210 finding nums in range 500 divisible by 3 Done w/ nums in range 100052 divisible by 3210 Done w/ nums in range 500 divisible by 3 Done w/ nums in range 508000 divisible by 34113 [0, 34113, 68226, 102339, 136452, 170565, 204678, 238791, 272904, 307017, 341130, 375243, 409356, 443469, 477582]
That's all for the basics, now for some things to watch out for!
What happens if we don't wait? What if we remove await in the main?
async def main(): divs1 = loop.create_task(find_divisibles(508000, 34113)) divs2 = loop.create_task(find_divisibles(100052, 3210)) divs3 = loop.create_task(find_divisibles(500, 3)) #await asyncio.wait([divs1, divs2, divs3]) return divs1, divs2, divs3
Then comment out the asking for the result:
if __name__ == '__main__': loop = asyncio.get_event_loop() d1, d2, d3 = loop.run_until_complete(main()) # Access us! #print(d1.result()) loop.close()
Run this:
finding nums in range 508000 divisible by 34113 finding nums in range 100052 divisible by 3210 finding nums in range 500 divisible by 3
Our tasks did begin, but they did not finish before our program exited, and no breaking exceptions were triggered. That's why we need to wait for things!
We can also use loop.set_debug(1)
, which will help us considerably to find errors like this. Add that in, and comment out the await. Now the alarms go off!
Another mistake I found myself making was attempting to use await
in a function (without async
in front of the def). This will result in a syntax error.
Finally, you should probably make use of finally
. You try to do your asyncio stuff, then, maybe hit some exceptions, and then finally you want to cleanup/close the loop with:
if __name__ == "__main__": try: loop = asyncio.get_event_loop() loop.set_debug(1) d1, d2, d3 = loop.run_until_complete(main()) print(d1.result()) except Exception as e: # logging...etc pass finally: loop.close()
Full script up to this point:
import asyncio async def find_divisibles(inrange, div_by): print("finding nums in range {} divisible by {}".format(inrange, div_by)) located = [] for i in range(inrange): if i % div_by == 0: located.append(i) if i % 50000 == 0: await asyncio.sleep(0.0001) print("Done w/ nums in range {} divisible by {}".format(inrange, div_by)) return located async def main(): divs1 = loop.create_task(find_divisibles(508000, 34113)) divs2 = loop.create_task(find_divisibles(100052, 3210)) divs3 = loop.create_task(find_divisibles(500, 3)) await asyncio.wait([divs1,divs2,divs3]) return divs1, divs2, divs3 if __name__ == "__main__": try: loop = asyncio.get_event_loop() loop.set_debug(1) d1, d2, d3 = loop.run_until_complete(main()) print(d1.result()) except Exception as e: # logging...etc pass finally: loop.close()
Other than that, I hope you have learned something useful! Maybe more on Asyncio in the future, but, for now, go and make some programs that don't hang up waiting for responses, or for some single long-running calculation to take place!