Select Page

When I started learning Go I quickly learned about the benefits of concurrent programing. So when I got a grasp of it in Go, I wondered if Python supported the same thing. Surely Python does, give it is so popular and a modern scripting language. The problem I found is that the examples available online are not very good. They are also mostly outdated. This is the Python Asyncio Walkthrough: So Easy Your Grandmother Can Do It.

What is Asyncio?

Concurrency is a feature of a modern programming language. For popular scripting languages like Python, Java, and C# new libraries are made to support them. Take a look at C#’s implementation, it’s not great.

import System.Threading.Tasks;

static async Task Main(string[] args){
     Task<MyTask> myTask = await DoTask();
}

Looking at the official documentation for the library we can see why.

Asyncio is a library to write concurrent code with the async/await syntax

docs.python.org

Here’s What You Need

  • Asyncio module
  • Python 3
  • Computer running VS Code

If you try running the program using Python 2.x then you will get an incorrect syntax error. This is because the library has been updated since then, including a change in syntax.

Quick Python Lesson – args and kvargs

# EXAMPLE 1
#!/usr/bin/python

import asyncio

async def do_task(a:*args) -> None:

    return print(args)

loop = asyncio.get_event_loop()
loop.run_until_complete(do_task("alpha","bravo","foo","bar"))
loop.close()

Accepting both arguments and keyword arguments in the same program:

# EXAMPLE 2
#!/usr/bin/python

import asyncio

async def do_task(*args, **kvargs):

    return print(args, kvargs)

loop = asyncio.get_event_loop()

loop.run_until_complete(do_task("bravo","foo","bar",last_arg="alpha"))
loop.close()
macbook$ python3 async-example.py 
('alpha', 'bravo', 'foo', 'bar')

Sometimes you want to accept a combination of arguments for flexibility. Running the program above in example two that accepts both keyword arguments and non keyword arguments demonstrates this idea.

macbook$ python3 async-example.py 
('bravo', 'foo', 'bar') {'last_arg': 'alpha'}

Quick Python Lesson – What is that name != main thing?

All modules in Python have an attribute named __name__ and if the program calling them is not the main program it will be equal to the name of the module.

# this is mymodule.py
a = 'hi from the package-example module, ' + __name__

Running the example program below demonstrates this concept.

# MODULE EXAMPLE 

# this is test-my-module.py
from mymodule import *

print(a)
print("but im saying hi from main, ' + __name__)

$ python3 test-my-module.py 
hi from the package-example module, mymodule
but im saying hi from main, __main__

This will run

async def do_task_A() -> None:
    print("starting Task A")
    print("done with Task A")

async def do_task_B() -> None:
    print("starting with Task B")
    print("running Task B will take some time, standby....")
    time_spent = time.perf_counter()
    time.sleep(5)
    print(f"done with Task B, it took {time_spent}")

# can NOT call await outside a function so that is why you see this function in asyncio tutorials!!

async def main():
    await asyncio.gather(do_task_B(), do_task_A())

if '__name__' != '__main__':
  asyncio.run(main())

Blocking prevents the program’s execution from continuing in the meantime.

starting with Task B
running Task B will take some time, standby....
done with Task B, it took 0.163988109
starting Task A
done with Task A

Same Example Using Asyncio

async def do_task_A() -> None:
    print("starting Task A")
    print("done with Task A")

async def do_task_B() -> None:
    print("starting with Task B")
    print("running Task B will take some time, standby....")
    time_spent = time.perf_counter()
    asyncio.sleep(5)
    #time.sleep(5)
    print(f"done with Task B, it took {time_spent}")

# can NOT call await outside a function so that is why you see this function in asyncio tutorials!!

async def main():
    await asyncio.gather(do_task_B(), do_task_A())

if '__name__' != '__main__':
  asyncio.run(main())

Replacing the thread-blocking time.spleep() function with asyncio’s asychronous waiting function causes a completely different result! The program’s execution is instant and while the function takes around 5 seconds as well, there is no waiting.

starting with Task B
running Task B will take some time, standby....
done with Task B, it took 0.051164088
starting Task A
done with Task A

The Subprocess Module

Sure asyncio is cool as you can tell, but to really understand it I want to know what problem it is solving. Taking a look at the subprocess module, I see one possible reason. Spawning a process and then reading it’s standard error and standard out is blocking. Meaning nothing else can happen. Asyncio allows for multiple processes to run at the same time.

proc = subprocess.Popen(['mycmd', "myarg"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)  
(stdout, stderr) = proc.communicate()

error: