6

I have a long-running synchronous Python program, where I'd like to run ~10 "fire and forget" tasks every second. These tasks hit a remote API and do not need to return any value. I tried this answer, but it requires too much CPU/memory to spawn and maintain all the separate threads so I've been looking into asyncio.

This answer explained nicely how to run "fire and forget" using asyncio. However, it requires using run_until_complete(), which waits until all the asyncio tasks are done. My program is using sync Python so this doesn't work for me. Ideally, the code should be as simple as this, where log_remote won't block the loop:

while True:
    latest_state, metrics = expensive_function(latest_state)
    log_remote(metrics) # <-- this should be run as "fire and forget"

I'm on Python 3.7. How can I easily run this using asyncio on another thread?

pir
  • 5,513
  • 12
  • 63
  • 101

1 Answers1

7

You can start a single event loop in a single background thread and use it for all your fire&forget tasks. For example:

import asyncio, threading

_loop = None

def fire_and_forget(coro):
    global _loop
    if _loop is None:
        _loop = asyncio.new_event_loop()
        threading.Thread(target=_loop.run_forever, daemon=True).start()
    _loop.call_soon_threadsafe(asyncio.create_task, coro)

With that in place, you can just call fire_and_forget on a coroutine object, created by calling an async def:

# fire_and_forget defined as above

import time

async def long_task(msg):
    print(msg)
    await asyncio.sleep(1)
    print('done', msg)

fire_and_forget(long_task('foo'))
fire_and_forget(long_task('bar'))
print('continuing with something else...')
time.sleep(3)

Note that log_remote will need to actually be written as an async def using asyncio, aiohttp instead of requests, etc.

user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • Doesn't this create a new thread everytime rather than using one single continuously running separate thread? – Alexis.Rolland Sep 13 '21 at 10:45
  • 1
    @Alexis.Rolland No, `fire_and_forget()` is careful to create/start a new thread only the first time it's invoked. – user4815162342 Sep 13 '21 at 11:53
  • How will threads work with asyncio if we have the python GIL which permits running only one thread at a time? Does the thread really exit when the async I/o task completes? – thegreatcoder Sep 07 '22 at 00:38
  • @thegreatcoder The GIL allows a single thread to run at once, this preventing you from utilizing multiple cores. The code you run in multiple threads will still appear to run in parallel, akin to code executed on a single-core system. In this setup the thread that runs the event loop is started only once and doesn't exit when the async task completes; it remains sleeping in the background, waiting for further taska. – user4815162342 Sep 07 '22 at 05:45
  • Thanks for the clarification. So the thread will be run in the background when the main thread exits the above program? Trying to understand when the extra thread will actually get executed if we have the GIL. – thegreatcoder Sep 07 '22 at 16:59
  • @thegreatcoder The thread will start the first time `fire_and_forget()` is invoked and will run in the background for as long as the program runs. Since it's a daemon thread, it won't prevent the program from exiting - so when the main thread is done with its work, the background thread will exit as well. Forget about the GIL, as it has nothing to do with any of that. The GIL just makes sure that CPU-bound code is not executing in parallel, but is invisibly chunked-up in a preemptive-multitasking kind of way. It's completely orthogonal to asyncio and doesn't affect IO-bound code. – user4815162342 Sep 07 '22 at 17:30
  • Ok. And by adopting a sync, the developer is incorporating cooperative multi tasking between various coroutines that run on the same thread, is that right? So if the function run by the thread is not async, second call of fire_and_forget will run only after the first call of the fire_and_forget is completed. With async, we’re manually providing ways for the execution to be interleaved. Is that correct? – thegreatcoder Sep 07 '22 at 20:28
  • @thegreatcoder *So if the function run by the thread is not async, second call of fire_and_forget will run only after the first call of the fire_and_forget is completed.* - The function has to be async, or you get an exception. But if the async function does something _blocking_, it will block the thread that runs the event loop. In this setup `fire_and_forget()` will still return immediately (it always does), but the next async task spawned by a subsequent call to `fire_and_forget` will not run until the event loop is unblocked. – user4815162342 Sep 07 '22 at 21:37
  • In case it's not obvious, note that the usage of asyncio presented in this answer is _not_ typical or idiomatic. It solves a very specific issue; the way it solves it is almost certainly not best practice. It is also not the greatest example for learning the basics of asyncio, nor of threading. – user4815162342 Sep 07 '22 at 21:40