1

I'm trying to find a solution to call async function in a Synchronous context.

And following is my references:

But I find that, asyncio.get_event_loop() fails when following asyncio.run(), here is my code to reproduce this issue:

import asyncio

async def asyncfunction(n):
    print(f'before sleep in asyncfunction({ n })')
    await asyncio.sleep(1)
    print(f'after sleep in asyncfunction({ n })')
    return f'result of asyncfunction({ n })'

def callback(r):
    print(f'inside callback, got: {r}')

r0 = asyncio.run(asyncfunction(0)) # cause following asyncio.get_event_loop() fail.
callback(r0)
print('sync code following asyncio.run(0)')

r1 = asyncio.run(asyncfunction(1)) # but following asyncio.run() still works.
callback(r1)
print('sync code following asyncio.run(1)')

async def wrapper(n):
    r = await asyncfunction(n)
    callback(r)

asyncio.get_event_loop().create_task(wrapper(2)) #fail if there  is asyncio.run() before
print('sync code following loop.create_task(2)')

#RuntimeError: There is no current event loop in thread 'MainThread'.

asyncio.get_event_loop().create_task(wrapper(3)) #the second call works if there is no asyncio.run() before
print('sync code following loop.create_task(3)')

# main

_all = asyncio.gather(*asyncio.all_tasks(asyncio.get_event_loop()))
asyncio.get_event_loop().run_until_complete(_all)

I think it might because that the event loop is "consumed" by something somehow, and asyncio.set_event_loop(asyncio.new_event_loop()) might be a workaround, but I'm not sure whether that is an expected usage for end-user to set event loop mannually. And I'm also wondering why and how everything happens here.


After read some of the source code of asyncio.run. I can know why this happens.

But I'm still wondering what is the expected way to call async function in a Synchronous context ?

It seems that the following code works (set a new event loop after each asyncio.run() call) :

asyncio.run(asyncfunction()) 
asyncio.set_event_loop(asyncio.new_event_loop())

but that is somehow weird, and doesn't seems to be the expected way.

luochen1990
  • 3,689
  • 1
  • 22
  • 37
  • According to the docs for `asyncio.run`: “ This function always creates a new event loop and closes it at the end.” – dirn Dec 03 '21 at 13:12
  • @dirn Thank you for point out that. I added more description and sub question now. – luochen1990 Dec 03 '21 at 13:24
  • See this answer: https://stackoverflow.com/questions/69710875/how-can-i-have-a-synchronous-facade-over-asyncpg-apis-with-python-asyncio/69732605#69732605 – Paul Cornelius Dec 03 '21 at 20:24

1 Answers1

2

When you call asyncio.run it creates a new event loop each time you call it. That event loop is subsequently destroyed when asyncio.run finishes. So in your code example after your second asyncio.run finishes there is no event loop at all, the two you've previously created don't exist anymore. asyncio.get_event_loop will normally create a new event loop for you unless set_event_loop was previously called, which asyncio.run does do (which explains why if you remove asyncio.run things work). To fix your code, you should create a new event loop and just use that instead of calling get_event_loop, bear in mind that this is a third loop and that may not be what you want.

import asyncio

async def asyncfunction(n):
  print(f'before sleep in asyncfunction({ n })')
  await asyncio.sleep(1)
  print(f'after sleep in asyncfunction({ n })')
  return f'result of asyncfunction({ n })'

def callback(r):
  print(f'inside callback, got: {r}')

r0 = asyncio.run(asyncfunction(0))
callback(r0)
print('sync code following asyncio.run(0)')

r1 = asyncio.run(asyncfunction(1))
callback(r1)
print('sync code following asyncio.run(1)')

async def wrapper(n):
  r = await asyncfunction(n)
  callback(r)

loop = asyncio.new_event_loop()
loop.create_task(wrapper(2))
print('sync code following loop.create_task(2)')


loop.create_task(wrapper(3))
print('sync code following loop.create_task(3)')

# main

_all = asyncio.gather(*asyncio.all_tasks(loop))
loop.run_until_complete(_all)
Matt Fowler
  • 2,563
  • 2
  • 15
  • 18
  • Thanks for your answer, but could you please point out **what is the expected way to call async function in a Synchronous context** in python asyncio ? – luochen1990 Dec 03 '21 at 13:27
  • @luochen1990 it depends on your situation. Normally you would create an event loop manually and call `loop.run_until_complete(async_function)` but creating a task as you've done works if you need it to return instantly. – Matt Fowler Dec 03 '21 at 13:31
  • So you mean `asyncio.new_event_loop().run_until_complete(async_function())` ? – luochen1990 Dec 03 '21 at 13:33
  • @luochen1990 if you want a new event loop created each time then yes, but that seems unneeded. You can create one event loop with `loop = asyncio.new_event_loop()` and reuse that across multiple calls with `loop.run_until_complete` – Matt Fowler Dec 03 '21 at 13:36
  • From https://stackoverflow.com/a/56662635/1608276 , "run_until_complete is not for running any number of arbitrary async functions synchronously, it is for running the main entry point of your entire async program. This constraint is not immediately apparent from the docs." I'm not sure which one is correct.... :( – luochen1990 Dec 03 '21 at 13:43
  • @luochen1990 The problem is that you are not using asyncio the way it is designed to be used, which is that everything runs inside async code, which you start with a **single** invocation of `asyncio.run()`. The answer provides a way to do what you want, but you still seem dissatisfied. You cannot have it both ways: you need to either adapt your program to use asyncio correctly (which we can help you with **if** you tell us what you're trying to do), or settle for a hacky solution. – user4815162342 Dec 04 '21 at 11:22
  • @user4815162342 I'm solving this issue because I'm providing an SDK for application level progammer to access infra level ability, the infra layer only provide async api to me, and I have to provide sync API to my user. So I cannot change my user's main function to be async to call asyncio as the way it expecting. – luochen1990 Dec 05 '21 at 12:37
  • @user4815162342 And, if asyncio is not expected to be used this way, then which package should I use to solve this problem? – luochen1990 Dec 05 '21 at 12:38
  • @luochen1990 In that case, the optimal choice depends on what the async APIs you invoke are doing. If they are simple and don't depend on background tasks, then you can create a single event loop with `asyncio.new_event_loop()`, cache it inside your objects, and have your sync functions return `self._loop.run_until_complete(some_async_fun())`. Provide a `close()` method that will call `self._loop.close()` and you're done. In other words, **don't** use `asyncio.run()`, because it will (for your use case) do the exact wrong thing, creating a whole new event loop each time around. – user4815162342 Dec 05 '21 at 12:44
  • @luochen1990 If the underlying APIs you call are more involved and expect the loop to run the whole time (because they have background tasks like watchdogs or similar), then create a background thread, run the event loop in it, and have your sync code return `asyncio.run_coroutine_threadsafe(some_async_fun(), self._loop).result()`. Your `close()` method will get rid of it using `self._loop.call_soon_threadsafe(self._loop.stop)`. That's a bit more complex, but it gives asyncio the illusion that it's run the usual way, and gives your callers a sync API. – user4815162342 Dec 05 '21 at 12:46
  • @user4815162342 Thank you! In my case, the async function I call is only RPC request, so I think it is the first case. But I don't know what `asyncio.new_event_loop()` is doing, and is that necessary to reuse it and handle the closing things manually, can I just create a new one per call and close it immediately after used ? – luochen1990 Dec 05 '21 at 13:08
  • @luochen1990 `new_event_loop()` creates a new event loop, which includes potentially heavy-weight operations like new file descriptor for communicating with foreign threads, etc. If you can, it's definitely better to reuse a single event loop across the board. If you create a new one, you can simply call `asyncio.run()`, which calls `new_event_loop()` under the hood. – user4815162342 Dec 07 '21 at 13:02
  • @user4815162342 asyncio.run() will close the event loop at the end. and we also cannot nest loop.run_until_complete() inside another loop.run_until_complete() call, do you have any idea about how to solve this issue? – luochen1990 Dec 08 '21 at 02:37
  • @luochen1990 You shouldn't care about `asyncio.run` closing the event loop because if you use `asyncio.run` consistently, each `asyncio.run` will create a new event loop. If on the other hand you choose to create a single loop and use `loop.run_until_complete` consistently, no one will close that loop. I don't understand where the nesting issue comes from, as you said you needed asyncio for "a simple RPC request". This seems like material for a new question, hopefully one that better clarifies what you are attempting to do and what problem you need solved. – user4815162342 Dec 09 '21 at 17:14
  • @user4815162342 Do you know the package named `nest-asyncio` and the issue behand it https://bugs.python.org/issue22239 ? – luochen1990 Dec 10 '21 at 01:32