[英]How to combine Celery with asyncio?
如何创建一个使 celery 任务看起来像asyncio.Task
的包装器? 或者有没有更好的方法将 Celery 与asyncio
集成?
@asksol,Celery 的创建者说:
使用 Celery 作为异步 I/O 框架之上的分布式层是很常见的(重要提示:将 CPU 绑定任务路由到 prefork worker 意味着它们不会阻塞您的事件循环)。
但是我找不到任何专门针对asyncio
框架的代码示例。
编辑:2021 年 1 月 12 日以前的答案(在底部找到)没有很好地老化,因此我添加了可能的解决方案组合,这些解决方案可能会满足那些仍然在寻找如何共同使用 asyncio 和 Celery 的人
让我们先快速分解用例(更深入的分析: 异步和协程 vs 任务队列):
因此,在 Python 的“做一件事并做好”的上下文中,不要尝试将 asyncio 和 celery 混合在一起是有意义的。
但是,如果我们希望能够以异步方式和异步任务的形式运行方法,会发生什么情况? 那么我们有一些选择要考虑:
我能找到的最好的例子如下: https ://johnfraney.ca/posts/2018/12/20/writing-unit-tests-celery-tasks-async-functions/(我刚刚发现这是@Franey 的回应):
定义你的异步方法。
使用asgiref
的sync.async_to_sync
模块来包装 async 方法并在 celery 任务中同步运行它:
# tasks.py import asyncio from asgiref.sync import async_to_sync from celery import Celery app = Celery('async_test', broker='a_broker_url_goes_here') async def return_hello(): await asyncio.sleep(1) return 'hello' @app.task(name="sync_task") def sync_task(): async_to_sync(return_hello)()
我在FastAPI应用程序中遇到的一个用例与前面的示例相反:
一个密集的 CPU 绑定进程正在占用异步端点。
解决方案是将异步 CPU 绑定进程重构为一个 celery 任务,并从 Celery 队列中传递一个任务实例以执行。
该案例可视化的最小示例:
import asyncio import uvicorn from celery import Celery from fastapi import FastAPI app = FastAPI(title='Example') worker = Celery('worker', broker='a_broker_url_goes_here') @worker.task(name='cpu_boun') def cpu_bound_task(): # Does stuff but let's simplify it print([n for n in range(1000)]) @app.get('/calculate') async def calculate(): cpu_bound_task.delay() if __name__ == "__main__": uvicorn.run('main:app', host='0.0.0.0', port=8000)
另一个解决方案似乎是@juanra和@danius在他们的答案中提出的,但我们必须记住,当我们混合同步和异步执行时,性能往往会受到影响,因此在我们决定使用之前,这些答案需要监控他们在生产环境中。
最后,有一些现成的解决方案,我不能推荐(因为我自己没有使用过),但我会在这里列出它们:
好吧,它的年龄没有那么好,是吗? Celery 5.0 版没有实现异步兼容性,因此我们不知道何时以及是否会实现......出于响应遗留原因(因为它是当时的答案)和评论继续,将其留在这里。
如官方网站所述,这可以从 Celery 5.0 版中实现:
http://docs.celeryproject.org/en/4.0/whatsnew-4.0.html#preface
以上内容来自上一个链接。
所以最好的办法就是等待5.0 版本发布!
与此同时,快乐的编码:)
这种简单的方法对我来说效果很好:
import asyncio
from celery import Celery
app = Celery('tasks')
async def async_function(param1, param2):
# more async stuff...
pass
@app.task(name='tasks.task_name', queue='queue_name')
def task_name(param1, param2):
asyncio.run(async_function(param1, param2))
您可以使用run_in_executor
将任何阻塞调用包装到任务中,如文档中所述,我还在示例中添加了自定义超时:
def run_async_task(
target,
*args,
timeout = 60,
**keywords
) -> Future:
loop = asyncio.get_event_loop()
return asyncio.wait_for(
loop.run_in_executor(
executor,
functools.partial(target, *args, **keywords)
),
timeout=timeout,
loop=loop
)
loop = asyncio.get_event_loop()
async_result = loop.run_until_complete(
run_async_task, your_task.delay, some_arg, some_karg=""
)
result = loop.run_until_complete(
run_async_task, async_result.result
)
这是一个简单的助手,您可以使用它来使 Celery 任务可等待:
import asyncio
from asgiref.sync import sync_to_async
# Converts a Celery tasks to an async function
def task_to_async(task):
async def wrapper(*args, **kwargs):
delay = 0.1
async_result = await sync_to_async(task.delay)(*args, **kwargs)
while not async_result.ready():
await asyncio.sleep(delay)
delay = min(delay * 1.5, 2) # exponential backoff, max 2 seconds
return async_result.get()
return wrapper
像sync_to_async
一样,它可以用作直接包装器:
@shared_task
def get_answer():
sleep(10) # simulate long computation
return 42
result = await task_to_async(get_answer)()
...作为装饰者:
@task_to_async
@shared_task
def get_answer():
sleep(10) # simulate long computation
return 42
result = await get_answer()
当然,这不是一个完美的解决方案,因为它依赖于polling 。 但是,在Celery 官方提供更好的解决方案之前,从 Django 异步视图调用 Celery 任务应该是一个很好的解决方法。
编辑 2021/03/02:添加了对sync_to_async
的调用以支持急切模式。
我发现这样做的最干净的方法是将async
函数包装在asgiref.sync.async_to_sync
(来自asgiref
):
from asgiref.sync import async_to_sync
from celery.task import periodic_task
async def return_hello():
await sleep(1)
return 'hello'
@periodic_task(
run_every=2,
name='return_hello',
)
def task_return_hello():
async_to_sync(return_hello)()
我从我写的一篇博客文章中提取了这个例子。
我通过在celery-pool-asyncio库中结合 Celery 和 asyncio 解决了问题。
使用 asyncio 实现 Celery 的好方法:
import asyncio
from celery import Celery
app = Celery()
async def async_function(param):
print('do something')
@app.task()
def celery_task(param):
loop = asyncio.get_event_loop()
return loop.run_until_complete(async_function(param))
这是我在必要时处理异步协程的 Celery 实现:
包装 Celery 类以扩展其功能:
from celery import Celery
from inspect import isawaitable
import asyncio
class AsyncCelery(Celery):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.patch_task()
if 'app' in kwargs:
self.init_app(kwargs['app'])
def patch_task(self):
TaskBase = self.Task
class ContextTask(TaskBase):
abstract = True
async def _run(self, *args, **kwargs):
result = TaskBase.__call__(self, *args, **kwargs)
if isawaitable(result):
await result
def __call__(self, *args, **kwargs):
asyncio.run(self._run(*args, **kwargs))
self.Task = ContextTask
def init_app(self, app):
self.app = app
conf = {}
for key in app.config.keys():
if key[0:7] == 'CELERY_':
conf[key[7:].lower()] = app.config[key]
if 'broker_transport_options' not in conf and conf.get('broker_url', '')[0:4] == 'sqs:':
conf['broker_transport_options'] = {'region': 'eu-west-1'}
self.config_from_object(conf)
celery = AsyncCelery()
最简单的方法(恕我直言)对我有用,使用的 python 版本是3.9.6
TL,DR:在不同的线程中永远运行 asyncio eventloop,为每个 celery 工作线程启动 id 和停止。 通过异步任务分配可等待的东西。 阻塞等待 Future API 执行异步任务
import asyncio
from threading import Thread
from celery import Celery
app = Celery("workers", backend="rpc://", broker="pyamqp://guest:guest@localhost/")
# 1. define new thread, loop which would be launched in new thread
def thread_function(loop: asyncio.AbstractEventLoop):
asyncio.set_event_loop(loop)
loop.run_forever()
loop = asyncio.get_event_loop()
thread = Thread(target=thread_function, args=(loop,))
# NOTE: it's not started yet
# 2. define your async infinite tasks (optional)
# this is async iteration example
async def my_iterate_async_infinite():
count = 0
while True:
await asyncio.sleep(1)
print(f"async iteration {count=}")
count += 1
# 3. subscribe for celery worker signals to start and stop thread
from celery.signals import worker_process_init, worker_process_shutdown
@worker_process_init.connect
def handle_worker_process_init(sender, **_):
thread.start()
asyncio.run_coroutine_threadsafe(my_iterate_async_infinite(), loop)
@worker_process_shutdown.connect
def handle(sender, **_):
# uncomment and use if you need async teardown
# asyncio.run_coroutine_threadsafe(example_call_async_teardown_function(), loop).result()
loop.stop()
thread.join()
# 4. call async function and wait for it's result, within task
@app.task()
def my_task():
print("Call async function within task")
async def do_async_func():
await asyncio.sleep(1)
return "my_async_result"
res = asyncio.run_coroutine_threadsafe(do_async_func(), loop=loop).result()
print(f"{res=}") # would be "me_async_result"
对于偶然发现此问题的任何人,特别是在异步sqlalchemy
(即使用asyncio
扩展)和 Celery 任务方面寻求帮助,明确处置引擎将解决问题。 这个特定的示例与asyncpg
一起使用。
例子:
from sqlalchemy.ext.asyncio import (
AsyncSession,
create_async_engine,
)
from sqlalchemy.orm import sessionmaker
from asgiref.sync import async_to_sync
engine = create_async_engine("some_uri", future=True)
async_session_factory = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
@celery_app.task(name="task-name")
def sync_func() -> None:
async_to_sync(some_func)()
async def some_func() -> None:
async with get_db_session() as session:
result = await some_db_query(session)
# engine.dispose will be called on exit
@contextlib.asynccontextmanager
async def get_db_session() -> AsyncGenerator:
try:
db = async_session_factory()
yield db
finally:
await db.close()
await engine.dispose()
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.