Reputation: 13589
I want to write a library that mixes synchronous and asynchronous work, like:
def do_things():
# 1) do sync things
# 2) launch a bunch of slow async tasks and block until they are all complete or an exception is thrown
# 3) more sync work
# ...
I started implementing this using asyncio
as an excuse to learn the learn the library, but as I learn more it seems like this may be the wrong approach. My problem is that there doesn't seem to be a clean way to do 2
, because it depends on the context of the caller. For example:
asyncio.run()
, because the caller could already have a running event loop and you can only have one loop per thread.do_things
as async
is too heavy because it shouldn't require the caller to be async. Plus, if do_things
was async
, calling synchronous code (1
& 3
) from an async
function seems to be bad practice.asyncio.get_event_loop()
also seems wrong, because it may create a new loop, which if left running would prevent the caller from creating their own loop after calling do_things
(though arguably they shouldn't do that). And based on the documentation of loop.close
, it looks like starting/stopping multiple loops in a single thread won't work.Basically it seems like if I want to use asyncio
at all, I am forced to use it for the entire lifetime of the program, and therefore all libraries like this one have to be written as either 100% synchronous or 100% asynchronous. But the behavior I want is: Use the current event loop if one is running, otherwise create a temporary one just for the scope of 2
, and don't break client code in doing so. Does something like this exist, or is asyncio
the wrong choice?
Upvotes: 1
Views: 1055
Reputation: 155046
I can't use asyncio.run(), because the caller could already have a running event loop and you can only have one loop per thread.
If the caller has a running event loop, you shouldn't run blocking code in the first place because it will block the caller's loop!
With that in mind, your best option is to indeed make do_things
async and call sync code using run_in_executor
which is designed precisely for that use case:
async def do_things():
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, sync_stuff)
await async_func()
await loop.run_in_executor(None, more_sync_stuff)
This version of do_things
is usable from async code as await do_things()
and from sync code as asyncio.run(do_things())
.
Having said that... if you know that the sync code will run very briefly, or you are for some reason willing to block the caller's event loop, you can work around the limitation by starting an event loop in a separate thread:
def run_async(aw):
result = None
async def run_and_store_result():
nonlocal result
result = await aw
t = threading.Thread(target=asyncio.run, args=(run_and_store_result(),))
t.start()
t.join()
return result
do_things
can then look like this:
async def do_things():
sync_stuff()
run_async(async_func())
more_sync_stuff()
It will be callable from both sync and async code, but the cost will be that:
Upvotes: 3