Home > front end >  Python run non-blocking async function from sync function
Python run non-blocking async function from sync function

Time:10-21

Is there a way to call an async function from a sync one without waiting for it to complete?

My current tests:

  1. Issue: Waits for test_timer_function to complete
async def test_timer_function():
    await asyncio.sleep(10)
    return

def main():
    print("Starting timer at {}".format(datetime.now()))
    asyncio.run(test_timer_function())
    print("Ending timer at {}".format(datetime.now()))
  1. Issue: Does not call test_timer_function
async def test_timer_function():
    await asyncio.sleep(10)
    return

def main():
    print("Starting timer at {}".format(datetime.now()))
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    asyncio.ensure_future(test_timer_function())
    print("Ending timer at {}".format(datetime.now()))

Any suggestions?

CodePudding user response:

Async functions really do not run in the background: they run always in a single thread.

That means that when there are parallel tasks in async code (normal async code), it is only when you give a chance to the asyncio loop to run that those are executed - this happens when your code uses yield, call one of async for, async with or return from a function itself.

In non-async code, you have to enter the loop and give control to it, in order to the async code to run - that is what asyncio.run does - and asyncio.ensure_future does not: this call just registers a task to be executed, whenever the asyncio loop has time for it: but you return from the function without ever passing control to the async loop, so it just finishes.

One thing that can be done is to establish a secondary thread, where the asyncio code will run: this thread will run its asyncio loop, and you can comunicate with tasks in it by using global variables and normal thread data structures like Queues.

The minimal changes for you code are:

import asyncio
import threading

from datetime import datetime
now = datetime.now


async def test_timer_function():
    await asyncio.sleep(2)
    print(f"ending async task at {now()}")
    return

def run_async_loop_in_thread():
    asyncio.run(test_timer_function())

def main():
    print(f"Starting timer at {now()}")
    t = threading.Thread(target=run_async_loop_in_thread)
    t.start()
    print(f"Ending timer at {now()}")
    return t

if __name__ == "__main__":
    t = main()
    t.join()
    print(f"asyncio thread exited normally at {now()}")

(please, when posting Python code, include the import lines and lines to call your functions and make your code actually run: it is not a lot of boiler plate like may be needed in other languages, and turn your snippets in complete, ready to run, examples)

printout when running this snippet at the console:

Starting timer at 2022-10-20 16:47:45.211654
Ending timer at 2022-10-20 16:47:45.212630
ending async task at 2022-10-20 16:47:47.213464
asyncio thread exited normally at 2022-10-20 16:47:47.215417

CodePudding user response:

The answer is simply no. It's not gonna happen in a single thread.

First issue:
In your first issue, main() is a sync function. It stops at the line asyncio.run(test_timer_function()) until the event loop finishes its work.

What is its only task? test_timer_function! This task "does" give the control back to event loop but not to the caller main! So if the event loop had other tasks too, they would cooperate with each other. But within the tasks of the event loop, not between event loop and the caller.

So it will wait 10 seconds. There is no other one here to use this 10 seconds to do its work.

Second issue:
You didn't even run the event loop. Check documentation for ensure_future.

  • Related