Asyncio Basics - Asynchronous programming with coroutines




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!

The next tutorial:





  • Intermediate Python Programming introduction
  • String Concatenation and Formatting Intermediate Python Tutorial part 2
  • Argparse for CLI Intermediate Python Tutorial part 3
  • List comprehension and generator expressions Intermediate Python Tutorial part 4
  • More on list comprehension and generators Intermediate Python Tutorial part 5
  • Timeit Module Intermediate Python Tutorial part 6
  • Enumerate Intermediate Python Tutorial part 7
  • Python's Zip function
  • More on Generators with Python
  • Multiprocessing with Python intro
  • Getting Values from Multiprocessing Processes
  • Multiprocessing Spider Example
  • Introduction to Object Oriented Programming
  • Creating Environment for our Object with PyGame
  • Many Blobs - Object Oriented Programming
  • Blob Class and Modularity - Object Oriented Programming
  • Inheritance - Object Oriented Programming
  • Decorators in Python Tutorial
  • Operator Overloading Python Tutorial
  • Detecting Collisions in our Game Python Tutorial
  • Special Methods, OOP, and Iteration Python Tutorial
  • Logging Python Tutorial
  • Headless Error Handling Python Tutorial
  • __str__ and __repr_ in Python 3
  • Args and Kwargs
  • Asyncio Basics - Asynchronous programming with coroutines