How are coroutines implemented?

I’m looking for a document explaining the implementation of coroutines (async def). What is the low-level mechanism that makes it all work? PEP 492 only has the specification but doesn’t say anything about how it works internally. Is there such a document or should I just try to understand from the source code? I tried looking at event loops and selectors but I’m a bit lost in the forest.

2 Likes

Originally, they were built on top of generators abusing their feature to send values or exceptions inside. So before async/await syntax, you’d see frameworks asking you to use some decorator and yield. In Python 3.4 you’d use:

import asyncio

@asyncio.coroutine
def coro():
    x = '6'
    awaited_val = yield another_coro()
    return x + awaited_val

So with that the event loop would create an iterator from it = coro() and then start it with next(it), get the another_coro() from that, also run it and when there’s a result it’d do send(it, awaited_val_of_another_coro) or throw(it, exception_from_another_coro) (which is nice since it gives the user ability to try/except that line with yield and catch that exact exception happening in the other coroutine it awaits). Event loop does that in a loop in a single thread, which means that the control flow is either inside of a coroutine, or in the loop’s own orchestration code.

That yield statement suspends the coroutine and it’s a point where the event loop gets control from the coroutine back and tries to select something else to execute, which can be some other coroutine or a suspended task waiting to be continued. For suspended tasks it’d be conditional because they are waiting for something, another coroutine needs to finish.

Over time generator+decorator hack was replaced by async/await syntactic sugar. The current implementation of asyncio has interfaces for changing policies of how the scheduler picks new tasks and you can also substitute the event loop implementation by something else given that it has appropriate interfaces.

@asvetlov can share more details, though…

For the technical implementation details in the interpreter, probably the best document is Brett’s writeup here:

If you want to understand better how coroutines and event loops fit together, then I like this tutorial by Andre Louis Caron:

5 Likes

I have a correction on @webknjaz’s explanation. When using @asyncio.coroutine, you’re supposed to use yield from not yield. For yield from, see PEP 380, and for how it’s to be used with asyncio, see PEP 3156. Also see this doc (by the PEP 380 author) about how to implement coroutines without asyncio: http://www.cosc.canterbury.ac.nz/greg.ewing/python/yield-from/yield_from.html

This (yield from) still works in more recent Python 3 versions, it’s just better to use async def and await (PEP 492, Python 3.5).

3 Likes

Right, thanks for the correction @guido!
I just want to add that Python 2 based things like Twisted used to use pure yield because py2 does not have yield from.

Yeah, there’s a whole different style of coroutines possible with just yield, but it’s not used with asyncio. See PEP 342.

1 Like