Asyncio task vs coroutine

What are the criteria to determine when a coroutine should be wrapped in a task or not?

You should use a task when you want your coroutine to effectively run in the background. The code you’ve seen just awaits the coroutines directly because it needs them running in sequence. For example, consider an HTTP client sending a request and waiting for a response:

# these two don't make too much sense in parallel
await session.send_request(req)
resp = await session.read_response()

There are situations when you want operations to run in parallel. In that case asyncio.create_task is the appropriate tool, because it turns over the responsibility to execute the coroutine to the event loop. This allows you to start several coroutines and sit idly while they execute, typically waiting for some or all of them to finish:

dl1 = asyncio.create_task(session.get(url1))
dl2 = asyncio.create_task(session.get(url2))
# run them in parallel and wait for both to finish
resp1 = await dl1
resp2 = await dl2

# or, shorter:
resp1, resp2 = asyncio.gather(session.get(url1), session.get(url2))

As shown above, a task can be awaited as well. Just like awaiting a coroutine, that will block the current coroutine until the coroutine driven by the task has completed. In analogy to threads, awaiting a task is roughly equivalent to join()-ing a thread (except you get back the return value). Another example:

queue = asyncio.Queue()

# read output from process in an infinite loop and
# put it in a queue
async def process_output(cmd, queue, identifier):
    proc = await asyncio.create_subprocess_shell(cmd)
    while True:
        line = await proc.readline()
        await queue.put((identifier, line))

# create multiple workers that run in parallel and pour
# data from multiple sources into the same queue
asyncio.create_task(process_output("top -b", queue, "top")
asyncio.create_task(process_output("vmstat 1", queue, "vmstat")

while True:
    identifier, output = await queue.get()
    if identifier == 'top':
        # ...

In summary, if you need the result of a coroutine in order to proceed, you should just await it without creating a task, i.e.:

# this is ok
resp = await session.read_response()
# unnecessary - it has the same effect, but it's
# less efficient
resp = await asyncio.create_task(session.read_reponse())

To continue with the threading analogy, creating a task just to await it immediately is like running t = Thread(target=foo); t.start(); t.join() instead of just foo() – inefficient and redundant.

Leave a Comment