【问题标题】:"Fire and forget" python async/await“一劳永逸”python async/await
【发布时间】:2016-09-13 16:51:48
【问题描述】:

有时需要进行一些非关键的异步操作,但我不想等待它完成。在 Tornado 的协程实现中,您可以通过简单地省略 yield 关键字来“触发并忘记”异步函数。

我一直在试图弄清楚如何使用 Python 3.5 中发布的新 async/await 语法“解雇并忘记”。例如,一个简化的代码sn-p:

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

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

bar()

不过,bar() 永远不会执行,而是会收到运行时警告:

RuntimeWarning: coroutine 'async_foo' was never awaited
  async_foo()  # fire and forget "async_foo()"

【问题讨论】:

  • 相关? stackoverflow.com/q/32808893/1639625其实我觉得是复制品,但我不想即时欺骗它。有人可以确认吗?
  • @tobias_k,我不认为它是重复的。链接上的答案太宽泛,无法回答这个问题。
  • (1)您的“主”进程是否会一直运行下去?或者(2)你想让你的进程死掉但让被遗忘的任务继续他们的工作吗?或者(3)你更喜欢你的主进程在结束之前等待被遗忘的任务吗?

标签: python python-3.5 python-asyncio


【解决方案1】:

这并不完全是异步执行,但也许run_in_executor() 适合你。

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)

【讨论】:

  • 不错的简洁答案。值得注意的是executor 将默认调用concurrent.futures.ThreadPoolExecutor.submit()。我提到是因为创建线程不是免费的;每秒 1000 次即发即弃可能会给线程管理带来很大压力
  • 是的。在使用这种方法每秒创建约 5 个小型即发即弃任务后,我没有听从您的警告,并且经历了大幅放缓。不要在生产中将其用于长时间运行的任务。它会吃掉你的 CPU 和内存!
【解决方案2】:

更新:

如果您使用 Python >= 3.7,请将 asyncio.ensure_future 替换为 asyncio.create_task,这是一种更新、更好的方式 to spawn tasks


asyncio.Task 到“一劳永逸”

根据asyncio.Task 的python 文档,可以启动一些协程“在后台”执行asyncio.ensure_future 创建的任务不会阻塞执行(因此函数会立即返回!)。这看起来像是一种按照您的要求“开枪即忘”的方式。

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())

输出:

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

如果事件循环完成后任务正在执行怎么办?

请注意,asyncio 期望在事件循环完成时完成任务。因此,如果您将main() 更改为:

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')

程序完成后您会收到此警告:

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

为防止您在事件循环完成后只能await all pending tasks

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))

杀死任务而不是等待它们

有时您不想等待任务完成(例如,某些任务可能被创建为永远运行)。在这种情况下,您可以只 cancel() 他们而不是等待他们:

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)

输出:

Do some actions 1
echo
Do some actions 2
echo
Do some actions 3
echo

【讨论】:

  • 我复制并通过了第一个块,然后简单地运行它,由于某种原因,我得到:第 4 行 async def async_foo(): ^ 好像函数定义存在一些语法错误第 4 行:“async def async_foo():”我错过了什么吗?
  • @GilAllen 此语法仅适用于 Python 3.5+。 Python 3.4 需要旧语法(请参阅 docs.python.org/3.4/library/asyncio-task.html )。 Python 3.3 及以下版本根本不支持 asyncio。
  • @Sardathrion 我不确定任务是否指向创建它的线程上的某个位置,但没有什么能阻止您手动跟踪它们:例如,只需将线程中创建的所有任务添加到列表中到时候取消他们上面解释的方式。
  • 很好的答案。只是想指出,大多数时候不希望等待所有挂起的任务完成或取消所有挂起的任务。我认为需要有一种中间方式,理想情况下收集在“注册表”(列表、集合或其他内容)中关闭事件循环后要继续执行的任务,然后等待这些任务完成并取消所有其他的。
  • 请注意“Task.all_tasks() 自 Python 3.7 起已弃用,请改用 asyncio.all_tasks()”
【解决方案3】:

输出:

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

这是一个简单的装饰器函数,它将执行推到后台,控制线移动到代码的下一行。

主要优点是,您不必将函数声明为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()")

注意:检查我的另一个 answer,它使用普通的 thread 而不使用 asyncio

【讨论】:

  • 在使用这种方法每秒创建约 5 个即发即弃的小型任务后,我的速度明显变慢。不要在生产中将其用于长时间运行的任务。它会吃掉你的 CPU 和内存!
  • 与 Django 合作得很好。不需要 Celery 等。我用它来快速返回服务器响应到客户端的请求,然后执行服务器响应不依赖的剩余必要操作。包括带有 Django ORM 的操作,就像在通常的执行流中一样。
  • 请注意,这只适用于主线程;如果您在另一个线程(至少 Python 3.6 和 3.9)上尝试此操作,asyncio.get_event_loop() 会引发 RuntimeError。例如threading.Thread(target=lambda: asyncio.get_event_loop()).start() 进行测试。
  • 你不需要运行另一个线程。在主线程中声明装饰器并在任何你想要的地方使用装饰器。
【解决方案4】:

由于某种原因,如果您无法使用asyncio,那么这里是使用普通线程的实现。检查我的其他答案和谢尔盖的答案。

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()")

生产

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

【讨论】:

  • 如果我们只需要这个 fire_and_forget 功能而不需要 asyncio 的其他功能,那么使用 asyncio 会更好吗?有什么好处?
【解决方案5】:
def fire_and_forget(f):
    def wrapped(*args, **kwargs):
        threading.Thread(target=functools.partial(f, *args, **kwargs)).start()

    return wrapped

是上述更好的版本——不使用 asyncio

【讨论】:

    猜你喜欢
    • 2016-07-20
    • 2017-06-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-30
    • 2016-11-11
    相关资源
    最近更新 更多