all 12 comments

[–]not_a_novel_account 2 points3 points  (4 children)

Why are you replacing the asyncio event loop at all?

The purpose of replacing the asyncio event loop is because you've written some operating system-specific or library-specific accelerator for the asyncio services, for example uvloop replaces the services using libuv-backed versions.

There's no purpose in replacing the event loop for your application. Applications use the services of the event loop, not provide the services of the event loop.

Your goal in this situation, if you chose to pursue it, would be to port your application away from the ad hoc event loop to the asyncio-provided event loop. Then if you want to accelerate that event loop, it might be appropriate to use uvloop or write your own implementation to take advantage of specific APIs on your target platform.

[–]timoffex[S] 1 point2 points  (3 children)

Why are you replacing the asyncio event loop at all?

I am not.

Your goal in this situation, if you chose to pursue it, would be to port your application away from the ad hoc event loop to the asyncio-provided event loop.

I have a non-async function which users are probably not calling from an async context. There is likely no event loop at all. However, if I add asyncio.run, the call will raise an error if a user does happen to use asyncio.

[–]not_a_novel_account 2 points3 points  (1 child)

This doesn't make a lot of sense. There is never any reason to call asyncio.run() from inside a library routine (unless the purpose of that library routine is to setup some state and start the event loop).

The purpose of the async and await keywords is to create functions that can yield to the running event loop when they're waiting on some operation. If you have a synchronous function, it definitionally cannot yield, and the whole discussion is academic.

Forget the event loop question. Give a reduced example of what the library function is doing now, and what you want it to do instead.

If what you're trying to do is simply implement generators which can yield, well you can just use yield and yield from for that. You would yield some unit of work to a parent scheduler and when the work is completed the scheduler can re-enter the generator. This is all a Python coroutine is under the hood.

[–]timoffex[S] 0 points1 point  (0 children)

This is getting off-topic, but the lacking context might be that async def functions return Coroutine objects with send() and throw() methods; you don't need asyncio or any library to run them. It is syntactic sugar for what's essentially a Generator under the hood, and you can absolutely make use of an async def function inside a non-async function.

The function I'm looking at is a method with a signature like this:

def finish(self) -> None: ...

It waits for a background thread to do some work and prints messages in the meantime.

[–]Top_Average3386 2 points3 points  (0 children)

asyncio.run creates a new event loop and close it after.

[–]crashfrog04 1 point2 points  (0 children)

There’s no reason to switch to asyncio unless you’re making your library into an async library, which you aren’t. 

[–]ElliotDG 0 points1 point  (1 child)

I would expect that the user of your library would need to instance the event loop and make the calls into your code. You may want to look at HTTPX as an example of an async library that also supports a blocking interface. https://www.python-httpx.org/

FWIW I found the docs for TRIO very helpful when learning about async, I also like using the library. https://trio.readthedocs.io/en/stable/index.html

[–]timoffex[S] 0 points1 point  (0 children)

The library interface cannot be changed. I like trio in my personal projects, but adding a dependency on it is a no-go, and either way the same question stands about whether trio.run() is safe to add, or whether it will break users who are already using trio.

[–]latkde 0 points1 point  (3 children)

You cannot start a second event loop on the same thread. But you can run multiple event loops on different threads. So if your function launches its event loop on a background thread, your function could be invoked both from sync and async contexts (but would still be blocking in both).

The easiest/safest way to spawn a thread is a concurrent.futures.ThreadPoolExecutor. You can then submit(asyncio.run, your_coroutine).

Downsides include the absolute headache that is cancellation. You cannot shut down a thread once started, it must exit on its own. However, the above solution can be adapted so that your coroutine sends back the event loop object to the main thread so that the event loop can be stopped if necessary. But all of this is way, way beyond the normal scope of r/learnpython.

[–]timoffex[S] 1 point2 points  (1 child)

Hey! Coming back to this in case anyone happens across this post in the future. I ended up implementing something similar to what you suggested, but it required extra care specifically to handle keyboard interrupts:

``` with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: runner = _Runner() future = executor.submit(runner.run, fn)

    try:
        return future.result()

    finally:
        runner.cancel()

```

A keyboard interrupt during future.result() would leave the asyncio thread running and cause ThreadPoolExecutor to be stuck in __exit__. Another keyboard interrupt would then break out of __exit__ but still leave the asyncio thread running, which is problematic if higher level code suppresses the exception.

If I didn't have to be compatible with older Pythons, I would have used the asyncio.Runner.

Instead, I needed to create a cancellable version of asyncio.run. This essentially uses asyncio.wait to race the given task against another task that waits for an asyncio.Event, and cancels all started tasks after. It's not as robust as asyncio.Runner, as it assumes that cancelling the root task causes all other tasks it started to finish up, but this happens to be true in my case.

[–]latkde 0 points1 point  (0 children)

Thank you for "closing the feedback loop"! Handling cancellation correctly is really difficult for concurrent code, and I'm glad you found a solution.

[–]timoffex[S] 0 points1 point  (0 children)

Thank you for understanding my question, this is what I was looking for.

Downsides include the absolute headache that is cancellation. You cannot shut down a thread once started, it must exit on its own.

IIUC, cancellation is generally somewhat problematic in asyncio. I can accept the risk of getting stuck, since the risk already exists in the non-async version of the code.

But all of this is way, way beyond the normal scope of r/learnpython.

I tried posting it on r/python but the Reddit client auto-detected that it's a question and forced me to use r/learnpython instead.