Skip to main content

concurrency in python 3.x

·2 mins
Disclaimer: This is an unrefined post, see Consistency over Perfectionism.

Python 3.x introduced coroutines (subroutines which can be entered, exited, and resumed at many different points), which can be created and executed using the async/await syntax provided by the asyncio package.

Python is single-threaded, so there is a single event loop on which (co)routines can run, meaning that parallelism cannot be achieved: asyncio provides tools for concurrent programming.

  • async def turns a function into a coroutine
    • note: if the function itself doesn’t use await in its body, it will still run as a whole
  • await <coroutine> signals that the function is waiting for the result of the coroutine and is yielding control until then, so that other coroutine can run in the meanwhile: it does not block.

Coroutines do not actually run when simply invoked, as in the main of the following snippet

import asyncio

async def say_after(delay, what):
	await asyncio.sleep(delay)
	print(what)

async def main():
	say_after(delay, what)

asyncio.run(main()) 

There are three ways to run coroutines:

  • Using asyncio.run(<coroutine>), as in the last line of the above snippet

  • Sequentially:

async def main():
	await say_after(1, 'hello')
	await say_after(2, 'world')

asyncio.run(main()) 

# output:

# hello
# world

In this situation, the execution in the above snippet is sequential, not concurrent (the total elapsed time will be 1 + 2 = 3 seconds).

  • Concurrently (making use of asyncio.Task):
async def main():
	t1 = asyncio.create_task(say_after(1, 'hello'))
	t2 = asyncio.create_task(say_after(2, 'world'))

	await t1
	await t2

asyncio.run(main()) 

Now the execution will complete in 2 seconds, because asyncio.create_task schedules the task to “run soon” (as per docs).