[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