Skip to content

Soonify - Concurrent Code

The main point of async code is to be able to run things concurrently. That means running multiple async functions during the same period of time.

That way, while Python waits (in one of the await parts) for something to finish, it can work in another of the async functions.

Async Code - Not Concurrent

Let's start by just calling an async function 3 times, one after the other.

This is still not concurrent because Python will run each one in its turn:

# Code above omitted 👆

async def get_data():
    await do_work(name="Yury")
    await do_work(name="Nathaniel")
    await do_work(name="Alex")

# Code below omitted 👇
👀 Full file preview
# 🚨 This is not concurrent 🚨

import anyio


async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")


async def get_data():
    await do_work(name="Yury")
    await do_work(name="Nathaniel")
    await do_work(name="Alex")


async def main():
    await get_data()


anyio.run(main)

Python will see the first await, then it will know that this might take a while.

Now Python could run something else that was already waiting somewhere else.

But we didn't tell Python to run anything else during this same period of time.

So Python will just come back and wait for the first await to finish.

Then Python will see the next line to execute and will do all that again.

So, although we use async and await here, this code is not concurrent, at least not the calls to do_work().

If you have something else running it (for example, a FastAPI app), then it will let Python go and do other things during those awaits (for example, handle other requests). That way the program would take some advantage of async and await.

But the calls to do_work() are not concurrent inside of get_data().

Async Code - Concurrent

Let's now use Asyncer to run these 3 functions concurrently. 🎉

Task Group

Use Asyncer's create_task_group() in an async with block to create a task group object:

# Code above omitted 👆

import asyncer

# Code here omitted 👈

async def get_data():
    async with asyncer.create_task_group() as task_group:
        # Code here omitted 👈

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")


async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        task_group.soonify(do_work)(name="Nathaniel")
        task_group.soonify(do_work)(name="Alex")


async def main():
    await get_data()


anyio.run(main)

Task Group - Soonify One Function

Now use task_group.soonify() to tell it to run the first function soon (that's why it's called "soonify()").

Pass it the async function to call.

That returns another function that receives the arguments for the async function you want to call:

# Code above omitted 👆

import asyncer

# Code here omitted 👈

async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        # Code here omitted 👈

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")


async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        task_group.soonify(do_work)(name="Nathaniel")
        task_group.soonify(do_work)(name="Alex")


async def main():
    await get_data()


anyio.run(main)

This tells this task group to run that function soon.

It won't run it right away and make Python wait for it. Instead, the task group will first receive all the things you want to run and then call them concurrently: during the same period of time.

Task Group - Soonify More Functions

Now you can use the same task_group.soonify() to add the other async functions you want to call concurrently with their parameters:

# Code above omitted 👆

import asyncer

# Code here omitted 👈

async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        task_group.soonify(do_work)(name="Nathaniel")
        task_group.soonify(do_work)(name="Alex")
    # Code here omitted 👈

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")


async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        task_group.soonify(do_work)(name="Nathaniel")
        task_group.soonify(do_work)(name="Alex")


async def main():
    await get_data()


anyio.run(main)

After the async with block ends, Python takes it as if it had an implicit await there.

At that point, this task group will run those async functions concurrently.

Python will wait for all that to end in that async with block. It's like if the async with block had an implicit await at the end.

Review All the Code

import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")


async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        task_group.soonify(do_work)(name="Nathaniel")
        task_group.soonify(do_work)(name="Alex")


async def main():
    await get_data()


anyio.run(main)

Python will start with the first async function call to do_work() with the parameters name="Yury".

It will start that anyio.sleep(1) and it will notice the await there:

# Code above omitted 👆

async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")


async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        task_group.soonify(do_work)(name="Nathaniel")
        task_group.soonify(do_work)(name="Alex")


async def main():
    await get_data()


anyio.run(main)

That's the cue for Python to go and run anything else that might be pending.

At that point, Python will notice another thing pending to run, the next call to do_work() with the arguments name="Nathaniel". It will run it, notice the await, etc.

At some point it will get back to the first await and see that it's now done, so it will continue with that code, and print a message:

# Code above omitted 👆

async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")

# Code below omitted 👇
👀 Full file preview
import anyio
import asyncer


async def do_work(name: str):
    await anyio.sleep(1)
    print(f"Hello, {name}")


async def get_data():
    async with asyncer.create_task_group() as task_group:
        task_group.soonify(do_work)(name="Yury")
        task_group.soonify(do_work)(name="Nathaniel")
        task_group.soonify(do_work)(name="Alex")


async def main():
    await get_data()


anyio.run(main)

Because this was the first call, with name="Yury", it will print:

Hello, Yury

After that, this function call to do_work() is done. There's nothing else to run in it.

Then Python will continue with the next thing pending. And that will be another await for do_work() with the arguments name="Nathaniel".

So now it will print:

Hello, Nathaniel

At some point, Python will end running all the things from this task group.

That will be the end of the async with block for the task group, and Python will continue executing whatever is next.

Run the Program in the Command Line

If you run the program in the command line, you will see that it will be silent for around 1 second.

And then it will print about everything at once.

This is because Python was waiting for all those anyio.sleep(1) calls concurrently.

So, Python was waiting 1 second in 3 places. But in each of those, it started waiting at around the same time, so it will only take around 1 second for it to run, instead of 3 seconds:

$ python main.py

// Enjoy the silence...


// All the output at once! 🎉
Hello, Yury
Hello, Nathaniel
Hello, Alex

Type Support

Now, because of the way Asyncer is designed, you will get typing support for your code.

This means that you will have autocompletion in your editor for the original arguments of the async function to call:

And you will have inline errors in your editor too:

If you use tools like mypy, those tools will also be able to help you detect those possible errors.

This can help you write code more efficiently and with more confidence that it's correct. And it will also make sure that whenever you refactor it, you are not breaking anything somewhere else. 😎

Next Steps

Next I'll show you how to run async functions concurrently and retrieve their return values. 🚀