Learn Python Coroutines in Less Than 5 Minutes

Rahul Sharma
Xebia Engineering Blog
5 min readMar 2, 2020
Photo by Kenan Süleymanoğlu on Unsplash

Coroutines are computer program components that generalise subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed.

— Wikipedia

Before I explain Coroutines in Python I would like to explain the subject. We implement applications using classes and functions. These functions execute business logic as a sequence of machine instructions. The functions in turn call other functions to deliver the complete solution. It is important to note that all code gets executed without any suspension. These general purpose functions are also knows as subroutines. Let’s build clarity with a simple program which generates a countdown and performs a divisibility test on the numbers.

countdown sequence
countdown

First Coroutine

On the other hand coroutines are functions which can be suspended and resumed multiple times. The suspended execution enables Coroutines to build concurrency in an application. Coroutines have use cases in state-machine, Actors, Event loops, Server listeners etc.

Concurrency is defined the concept of execution of multiple functions in an interleaved manner. It is different from parallelism, which is defined as the simultaneous execution by utilising multi-core CPU architecture. Concurrency is often achieved with asynchronous data transfer or by using event based callbacks.

Python supports coroutines since 2.5 version. It improved the support in 3.5 by introducing async and await syntax. Coroutines are executed by using the asyncio package.

  • The async keyword is added to the method definition. It signifies that the method can suspend execution to wait for new events.
  • The await keyword suspends method execution and yields back the CPU to execute next task. The method resumes execution from the await statement after the invoking event has occurred.

Let’s now re-implement the above program using Coroutines.

countdown coroutine

We created a coroutine for the countdown function. Coroutines can’t be invoked directly as function calls. They need to be invoked using an external implementation like asyncio. Try to invoke tasks function and it will fail with the error coroutine ‘tasks’ was never awaited

The tasks coroutine provides important details of coroutine execution.. In order to execute our countdown coroutine we had to build a asyncio task. We then waited for the completion of the task. The tasks were executed by the asyncio engine after we invoked the run method. It is important to note that both the tasks get executed sequentially, one after other. Since the tasks do not give back control so there is no element of concurrency here.

Output log

Enable Concurrency

Concurrency can only be added if the methods suspend execution and give up CPU. In order to give-up cpu we add a sleep to the divisibility method. We can timer.sleep as it would consume the cpu. Instead we use asyncio.sleep method for the same. As we do this we convert the divisibleBy subroutine into a coroutine. It would need to have async keyword in its definitions and the invocation will need to be awaited.

Awaited divisibleBy

The above code produces concurrent output from both the tasks.

Concurrent output log

Producer-consumer Coroutines

The asyncio python package is quite versatile. It not only offers invocation of coroutines, but also supports various execution scenarios. We can perform data transfer between various coroutines using Queues. Lets re-look our example using queue.

Queue based countdown

The above code changes the solution of countdown and divisibleBy to a producer-consumer scenario. The countdown function is a producer which adds number to the queue. Next, we added two divisibleBy functions which are consumers of the numbers on the queue. In the end we are no longer awaiting for tasks to finish. Instead we have joined on the queue. The join functions makes sure that we await until the queue becomes empty. But, we await on the countdown task to finish. This would make sure that all numbers have been added to queue. If we do not await on it the program would exit, without any result. This is because no numbers will be added when the queue is joined on.

I have shown on queues but asyncio also have events and synchronisation constructs to support various other use-cases like state-transitions. It also enables us to build worker based event loops to execute tasks as well as processes. I had just scratched the surface of asyncio package. The above example was not very useful. It was only intended to explain the concepts. The package provides a number of APIs which can be used to build many use-cases like network listeners, batch processing engine, state-machines etc.

Threads Or Coroutines

Threads are often used to build concurrency and parallelism in an application.We create threads to execute a task in background while serving the main application. OS executes these threads in a concurrent manner, slicing CPU time between all of them. Moreover, if there are numerous tasks , then we can run more threads to finish these pending tasks. But there is a catch ! if tasks are CPU intensive and we create threads more than the number of CPU cores available then the complete application will exhibit degraded performance. The compute intensive tasks will compete for CPU cycles, making the OS to perform Context switching quite often.

On the other hand coroutines can’t be compute intensive. They must give back CPU and wait for next sequence of events. Unlike threads the OS will not perform context switch between different coroutines. If we build them compute intensive then the application will not meet the desired behaviour. It is important to note that Coroutines are supported by the language while Threads are supported by the OS. We can build coroutine behaviour using threads which wait for events in queues and then take an action.

--

--