200

Sometimes there is some non-critical asynchronous operation that needs to happen but I don't want to wait for it to complete. In Tornado's coroutine implementation you can "fire & forget" an asynchronous function by simply ommitting the yield key-word.

I've been trying to figure out how to "fire & forget" with the new async/await syntax released in Python 3.5. E.g., a simplified code snippet:

async def async_foo():
    print("Do some stuff asynchronously here...")

def bar():
    async_foo()  # fire and forget "async_foo()"

bar()

What happens though is that bar() never executes and instead we get a runtime warning:

RuntimeWarning: coroutine 'async_foo' was never awaited
  async_foo()  # fire and forget "async_foo()"
Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
Mike N
  • 6,395
  • 4
  • 24
  • 21
  • 3
    Related? http://stackoverflow.com/q/32808893/1639625 In fact, I think it's a duplicate, but I don't want to instant-dupe-hammer it. Can someone confirm? – tobias_k May 20 '16 at 10:00
  • 8
    @tobias_k, I don't think it's duplicate. Answer at the link is too broad to be answer for this question. – Mikhail Gerasimov May 20 '16 at 11:44
  • 3
    Does (1) your "main" process continue running forever ? Or (2) do you want to allow your process to die but allowing forgotten tasks continue their job ? Or (3) do you prefer your main process waiting for forgotten tasks just before ending ? – Julien Palard May 26 '16 at 08:23

5 Answers5

303

Upd:

Replace asyncio.ensure_future with asyncio.create_task everywhere if you're using Python >= 3.7 It's a newer, nicer way to spawn tasks.


asyncio.Task to "fire and forget"

According to python docs for asyncio.Task it is possible to start some coroutine to execute "in the background". The task created by asyncio.ensure_future won't block the execution (therefore the function will return immediately!). This looks like a way to "fire and forget" as you requested.

import asyncio


async def async_foo():
    print("async_foo started")
    await asyncio.sleep(1)
    print("async_foo done")


async def main():
    asyncio.ensure_future(async_foo())  # fire and forget async_foo()

    # btw, you can also create tasks inside non-async funcs

    print('Do some actions 1')
    await asyncio.sleep(1)
    print('Do some actions 2')
    await asyncio.sleep(1)
    print('Do some actions 3')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Output:

Do some actions 1
async_foo started
Do some actions 2
async_foo done
Do some actions 3

What if tasks are executing after the event loop has completed?

Note that asyncio expects tasks to be completed at the moment the event loop completes. So if you'll change main() to:

async def main():
    asyncio.ensure_future(async_foo())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(0.1)
    print('Do some actions 2')

You'll get this warning after the program finished:

Task was destroyed but it is pending!
task: <Task pending coro=<async_foo() running at [...]

To prevent that you can just await all pending tasks after the event loop has completed:

async def main():
    asyncio.ensure_future(async_foo())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(0.1)
    print('Do some actions 2')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    
    # Let's also finish all running tasks:
    pending = asyncio.Task.all_tasks()
    loop.run_until_complete(asyncio.gather(*pending))

Kill tasks instead of awaiting them

Sometimes you don't want to await tasks to be done (for example, some tasks may be created to run forever). In that case, you can just cancel() them instead of awaiting them:

import asyncio
from contextlib import suppress


async def echo_forever():
    while True:
        print("echo")
        await asyncio.sleep(1)


async def main():
    asyncio.ensure_future(echo_forever())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(1)
    print('Do some actions 2')
    await asyncio.sleep(1)
    print('Do some actions 3')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    # Let's also cancel all running tasks:
    pending = asyncio.Task.all_tasks()
    for task in pending:
        task.cancel()
        # Now we should await task to execute it's cancellation.
        # Cancelled task raises asyncio.CancelledError that we can suppress:
        with suppress(asyncio.CancelledError):
            loop.run_until_complete(task)

Output:

Do some actions 1
echo
Do some actions 2
echo
Do some actions 3
echo
SuperStormer
  • 4,997
  • 5
  • 25
  • 35
Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • I copied and past the first block and simply ran it on my end and for some reason I got: line 4 async def async_foo(): ^ As if there is some syntax error with the function definition on line 4: "async def async_foo():" Am I missing something? – Gil Allen Nov 06 '16 at 09:59
  • 4
    @GilAllen this syntax works only in Python 3.5+. Python 3.4 needs old syntax (see https://docs.python.org/3.4/library/asyncio-task.html ). Python 3.3 and below doesn't support asyncio at all. – Mikhail Gerasimov Nov 06 '16 at 14:14
  • How would you kill the tasks in a thread?…̣I have a thread that creates some tasks and I want to kill all the pending ones when the thread dies in its `stop()` method. – Sardathrion - against SE abuse Nov 24 '17 at 15:21
  • 1
    @Sardathrion I'm not sure if task points somewhere on thread in which it was created, but nothing stops you from to track them manually: for example, just add all tasks created in thread to a list and when time comes cancel them way explained above. – Mikhail Gerasimov Nov 24 '17 at 16:11
  • Sadly this does not work… [No idea why](https://stackoverflow.com/q/47514100/232794). ☹ – Sardathrion - against SE abuse Nov 27 '17 at 15:14
  • 2
    Great answer. Just would like to point out that it is most of the time not desirable to either wait for all pending tasks to finish OR to cancel all pending tasks. I think there need to be a middle way, ideally collecting the tasks which you want to keep executing after closing the event loop in a "registry" (list, set or something else) and then simply wait for those tasks to finish and cancel all the other ones. – Torsten Engelbrecht May 04 '18 at 01:33
  • 11
    Note that "Task.all_tasks() is deprecated since Python 3.7, use asyncio.all_tasks() instead" – Alexis Jun 12 '20 at 17:52
  • How do you use `loop.run_until_complete()` when you are using `web.run_app` from aiohttp? – CMCDragonkai Aug 14 '20 at 10:41
  • 1
    If cancelling multiple tasks, it might be better to gather them together instead of cancelling and awaiting each task sequentially: `for task in tasks: task.cancel()` and then `await asyncio.gather(tasks, return_exceptions=True)`. The `return_exceptions=True` ensures that `CancelledError`s are suppressed. – Anakhand May 21 '22 at 15:42
  • `asyncio.esure_future` is depreciated in [3.10](https://docs.python.org/3/library/asyncio-future.html#asyncio.ensure_future) – Jamie Marshall Jun 29 '22 at 17:57
  • Your answer contains bug: asyncio.ensure_future(echo_forever()) requires to save result of this function, to prevent it being deleted by gabage collector, even mid-execution. See docs: https://docs.python.org/3/library/asyncio-future.html – al.zatv Feb 15 '23 at 15:35
  • @al.zatv ah, you're right! Although this line appeared only in doc for Python 3.9+ I guess, this is relevant for older Python versions nevertheless. I'll edit the answer, thank you! – Mikhail Gerasimov Feb 15 '23 at 15:47
  • @al.zatv although, I just thought, and I think the manual reference is probably non-needed in this case: since the task is running forever `asyncio` should internally store the reference to it. I think only tasks that are going to be finished before used should be referenced. I'll check if I'm correct later and either edit or not edit the answer. But thank you for noting the line in docs anyway! – Mikhail Gerasimov Feb 15 '23 at 15:54
  • @user:1113207 Well, I also need to fire-and-forget tasks, but my case is more complicated than your example, so I have to take this into account. In python docs, they recommend this: https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task – al.zatv Feb 15 '23 at 16:11
32

Output:

>>> Hello
>>> foo() started
>>> I didn't wait for foo()
>>> foo() completed

Here is the simple decorator function which pushes the execution to background and line of control moves to next line of the code.

The primary advantage is, you don't have to declare the function as await

import asyncio
import time

def fire_and_forget(f):
    def wrapped(*args, **kwargs):
        return asyncio.get_event_loop().run_in_executor(None, f, *args, *kwargs)

    return wrapped

@fire_and_forget
def foo():
    print("foo() started")
    time.sleep(1)
    print("foo() completed")

print("Hello")
foo()
print("I didn't wait for foo()")

Note: Check my other answer which does the same using plain thread without asyncio.

nehem
  • 12,775
  • 6
  • 58
  • 84
  • 2
    I experienced substantial slowdown after using this approach creating ~5 small fire-and-forget tasks per second. Don't use this in production for a long-running task. It'll eat your CPU and memory! – pir Jul 27 '20 at 03:39
  • 1
    Good work with Django. Don't need Celery, etc. I use it to quickly return the server response to the client's request and then perform the remaining necessary actions that the server response does not depend on. Including actions with Django ORM, as in the usual stream of execution. – Роман Арсеньев Aug 21 '21 at 06:53
  • Note that this only works from the main thread; `asyncio.get_event_loop()` raises a `RuntimeError` if you try this on another thread (Python 3.6 and 3.9 at least). eg `threading.Thread(target=lambda: asyncio.get_event_loop()).start()` to test. – Tom Nov 26 '21 at 14:13
  • You wouldn't need to run this another thread. Declare the decorator in the main thread and use the decorator anywhere you would like. – nehem Nov 28 '21 at 02:08
  • Thanks! this solved my problem. – qzx Mar 07 '22 at 22:30
  • runs lots and lots of threads, not good – Erik Aronesty Apr 14 '22 at 19:42
  • @erik they will easily get garbage collected as long as your subroutines are reasonable – nehem Jan 09 '23 at 03:52
13

This is not entirely asynchronous execution, but maybe run_in_executor() is suitable for you.

def fire_and_forget(task, *args, **kwargs):
    loop = asyncio.get_event_loop()
    if callable(task):
        return loop.run_in_executor(None, task, *args, **kwargs)
    else:    
        raise TypeError('Task must be a callable')

def foo():
    #asynchronous stuff here


fire_and_forget(foo)
Sergey Gornostaev
  • 7,596
  • 3
  • 27
  • 39
  • 8
    Nice concise answer. It is worth noting that the `executor` will default to calling `concurrent.futures.ThreadPoolExecutor.submit()`. I mention because creating threads is not free; fire-and-forgetting 1000 times a second will probably put a big strain on thread management – Brad Solomon Jan 08 '19 at 17:55
  • 1
    Yep. I didn't heed your warning and experienced substantial slowdown after using this approach creating ~5 small fire-and-forget tasks per second. Don't use this in production for a long-running task. It'll eat your CPU and memory! – pir Jul 27 '20 at 03:39
  • Is using Process executor better in this case then? @BradSolomon – Rami Awar Jun 12 '22 at 10:43
11

For some reason if you are unable to use asyncio then here is the implementation using plain threads. Check my other answers and Sergey's answer too.

import threading, time

def fire_and_forget(f):
    def wrapped():
        threading.Thread(target=f).start()

    return wrapped

@fire_and_forget
def foo():
    print("foo() started")
    time.sleep(1)
    print("foo() completed")

print("Hello")
foo()
print("I didn't wait for foo()")

produces

>>> Hello
>>> foo() started
>>> I didn't wait for foo()
>>> foo() completed
nehem
  • 12,775
  • 6
  • 58
  • 84
  • 3
    If we only need this fire_and_forget functionality and nothing else from asyncio, would it still be better to use asyncio? What's the benefits? – pir May 31 '20 at 17:26
  • @pir benefit is that you can have an async function in a web server for example, and have a task that you wanna run in the background but return the function asap. For example API route that connects to a DB (async client) then makes a call then closes that connection with DB (async close). I don't really want to await the DB connection close, I just want it to close eventually. I prefer to return the API call asap. This is a perfect use case for asyncio.ensure_future(client.close()) for example. Fire and forget it and return the API response. – Rami Awar Jun 11 '22 at 07:07
  • The problem here is you have no guarantee that the threads won't get out of hand. With `asyncio`, they just get '**scheduled**', but the **main thread** will eventually process the `foo` function. This anser is spawning thread's haphazardly every time the function is called and has no way to catch problems. This may seem similar to @nehem 's other answer, but it functionally is very different under the hood. – Jamie Marshall Jun 29 '22 at 17:44
-4
def fire_and_forget(f):
    def wrapped(*args, **kwargs):
        threading.Thread(target=functools.partial(f, *args, **kwargs)).start()

    return wrapped

is the better version of the above -- does not use asyncio

Clay Schubiner
  • 133
  • 1
  • 4