4

I'm writing an event-driven program with the event scheduling written in C. The program uses Python for extension modules. I would like to allow extension modules to use async/await syntax to implement coroutines. Coroutines will just interact with parts of my program, no IO is involved. My C scheduler is single-threaded and I need coroutines to execute in its thread.

In a pure Python program, I would just use asyncio as is, and let my program use its event loop to drive all events. This is however not an option; my event loop needs to serve millions of C-based events per second and I cannot afford Python's overheads.

I tried to write my own event loop implementation, that delegates all scheduling to my C scheduler. I tried a few approaches:

  • Re-implement EventLoop, Future, Task etc to imitate how asyncio works (minus IO), so that call_soon delegates scheduling to my C event loop. This is safe, but requires a bit of work and my implementation will always be inferior to asyncio when it comes to documentation, debugging support, intricate semantic details, and correctness/test coverage.
  • I can use vanilla Task, Future etc from asyncio, and only create a custom implementation of AbstractEventLoop, delegating scheduling to my C event loop in the same way. This is pretty straightforward, but I can see that the vanilla EventLoop accesses non-obvious internals (task._source_traceback, _asyncgen_finalizer_hook, _set_running_loop), so my implementation is still second class. I also have to rely on the undocumented Handle._run to actually invoke callbacks.
  • Things appeared to get simpler and better if I subclassed from BaseEventLoop instead of AbstractEventLoop (but docs say I shouldn't do that). I still need Handle._run, though.
  • I could spawn a separate thread that run_forever:s a vanilla asyncio.DefaultEventLoop and run all my coroutines there, but coroutines depend on my program's extension API, which does not support concurrent calls. So I must somehow make DefaultEventLoop pause my C event loop while calling Handle._run(). I don't see a reasonable way to achieve that.

Any ideas on how to best do this? How did others solve this problem?

Erik Carstensen
  • 634
  • 4
  • 14
  • 1
    Check [uvloop](https://uvloop.readthedocs.io/), it's written in `Cython`. You can use [asyncio.set_event_loop_policy](https://docs.python.org/3/library/asyncio-policy.html#asyncio.set_event_loop_policy) to change the event loop policy, for instance: `asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())` – HTF May 19 '21 at 17:16
  • `uvloop` is essentially my first bullet; a standalone reimplementation of `asyncio` using a different event loop. Unfortunately it does not fit my needs: `uvloop` is based on `libuv`'s event loop, and I can not move from my C loop to libuv (my loop is highly optimized for some domain-specific needs that are unrelated to IO) – Erik Carstensen May 19 '21 at 21:12
  • 1
    Given the constraints, your options are fairly limited. If your system is mostly active, you can use [this](https://stackoverflow.com/a/29797709/1600898) to run a single iteration of the event loop and schedule that in rapid succession. – user4815162342 May 20 '21 at 15:12
  • If you want to use only the public API, running the asyncio event loop in a different thread and communicating with it using `asyncio.run_coroutine_threadsafe` and friends is probably the best choice, but notice that it introduces pretty high overhead. (I once measured, to my shock, that `asyncio.run()`, which sets up and tears down a whole new event loop, is actually a bit cheaper than `asyncio.run_coroutine_threadsafe().result()`. Maybe this changed in the meantime, but it was bizarre.) – user4815162342 May 20 '21 at 15:16
  • Your extension API could perhaps detect that it's running outside your thread, and implement the appropriate communication? It's hard to tell what's the best choice, but they all look fairly unpleasant. Maybe you'll need to try to change at least some of the requirements to implement this cleanly. – user4815162342 May 20 '21 at 15:16
  • One option we are considering, is to abandon `async`/`await` and instead use fibers to implement coroutines in extension modules. We won't have that many coroutines so performance-wise this will be acceptable; my concern is if fibers are less pythonic than `async def`. Are they? – Erik Carstensen May 20 '21 at 22:26

1 Answers1

0

I found that trio, a third-party alternative to asyncio, provides explicit support for integration with alien event loops through something called guest mode. Solves my problem!

Erik Carstensen
  • 634
  • 4
  • 14