[Async-sig] Some thoughts on asynchronous API design in a post-async/await world

Vincent Michel vxgmichel at gmail.com
Mon Nov 7 07:51:58 EST 2016


Thanks Nathaniel for the great post!

It's indeed very impressive to see how curio worked his way around 
problems that I thought were part of any async library, by simply giving 
up on callbacks. One aspect of curio that I find particularly 
interesting is how it hides the event loop (or kernel) by:

1 - Using "yield (TRAP, *args)" as the main way to communicate with the 
event loop (no need to have it as a reference).
2 - Exposing a run function that starts the loop and make sure it safely 
terminates.

Even though there's a long way to go before we can have point 1 in 
asyncio (or at least a compatibility layer), I think point 2 is easy to 
implement and could bring something valuable.

So here's a proposal:

Add an asyncio.run function
===========================

... and promote it as the standard to run asynchronous applications.

Implementation
--------------

It could roughly be implemented as:

```
def run(main_coro, *, loop=None):
     if loop is not None:
         loop = asyncio.get_event_loop()
     try:
         return loop.run_until_complete(main_coro)
     finally:
         # More clean-up here?
         loop.close()
```

Example
-------

Instead of writing:

```
loop = asyncio.get_event_loop()
queue = asyncio.Queue(loop=loop)
producer_coro = produce(queue, 10)
consumer_coro = consume(queue)
gather = asyncio.gather(producer_coro, consumer_coro, loop=loop)
loop.run_until_complete(gather)
loop.close()
```

We could promote the following structure:

```
async def main():
     queue = asyncio.Queue()
     producer_coro = produce(queue, 10)
     consumer_coro = consume(queue)
     await asyncio.gather(producer_coro, consumer_coro)

if __name__ == '__main__':
     asyncio.run(main())
```

What do we get from that?
-------------------------

- A clear separation between the synchronous and the asynchronous world. 
Asynchronous objects should only be created inside an asynchronous context.
- No explicit vs implicit loop issues, PR #452 guarantees that objects 
created inside coroutines and callbacks will get the right event loop 
(so loop references can be omitted everywhere).
- The event loop disappears completely from the user code and becomes a 
low-level detail.
- It provides a proper way to clean things up after running the loop 
(e.g make sure all the pending callbacks are executed before the loop is 
closed, or maybe a curio-like behavior to wait for all "non-daemonic" 
tasks to complete.)

Any limitations?
----------------

One issue I can think of is the handling of KeyboardInterrupt. For 
instance, how to transform the TCP server example from the docs to fit 
the new standard? Ideally, we should be able to write something like this:

```
async def main():
     server = await asyncio.start_server(handle_echo, '127.0.0.1', 8888)
     print('Serving on {}'.format(server.sockets[0].getsockname()))
     await asyncio.wait_for_interrupt()
     server.close()
     await server.wait_closed())
```

But the handling of interrupts is not completely settled yet (see PR 
#305 and issue #341).

Also, some asyncio-based library might already implement a similar but 
specific run function. Could there be a conflict here?

Related topics
--------------

- [Explicit vs Implicit event loop discussion on python-tulip][1]
- [asyncio PR #452: Make get_event_loop() return the current loop if 
called from coroutines/callbacks][2]

[1]: https://groups.google.com/forum/#!topic/python-tulip/yF9C-rFpiKk
[2]: https://github.com/python/asyncio/pull/452


I hope it makes sense.
/Vincent


More information about the Async-sig mailing list