Django项目中Celery用@shared_task装饰任务调用卡死,换@app.task后正常
@app.task:任务在代码定义阶段就会直接和项目实例化的Celery App对象完成绑定,全链路没有延迟加载逻辑,任务的注册、调用、上下文加载都和当前App深度绑定,不存在二次初始化的触发点。@shared_task:设计初衷是解耦Django子应用和核心Celery实例的导入依赖,装饰的时候不会立刻绑定具体的App实例,会把任务标记为待绑定状态,等全局Celery App初始化到finalize阶段时,再统一把所有待绑定的任务挂载到对应的App上。
卡顿的根因(从Traceback可直接定位)
这类无限卡死是典型的Celery初始化互斥锁死循环,触发路径完全对应Traceback的栈帧:
- 第一次在Django Shell里调用
generate_pdf.delay()时,因为这是一个@shared_task装饰的待绑定任务,会触发Celery App的finalize初始化收尾流程,拿到_finalize_mutex互斥锁,开始批量绑定所有shared_task。 finalize流程执行到加载配置阶段,会触发on_after_configure信号,执行proj/celery.py中编写的setup_periodic_tasks回调(对应Traceback中指向代码celery.py:20的generate_weekly_report.s()行)。- 这个回调里引用的
generate_weekly_report同样是@shared_task装饰的待绑定任务,代码会再次触发finalize流程,尝试获取同一个_finalize_mutex锁。 - 因为锁已经被外层的finalize流程持有,代码会永远卡在锁等待状态,无法退出,直到触发KeyboardInterrupt中断。
观察到的“print语句已经输出但程序卡住”现象,是因为死循环发生前,栈帧已经执行到了generate_pdf函数里的print逻辑,但主流程卡在锁等待上无法返回执行结果。
为什么换成
@app.task就正常 被@app.task装饰的任务在定义时就已经完成了和App的绑定,不存在“待绑定”状态,调用.delay()、.s()方法时不会触发额外的finalize初始化流程,也就不会在on_after_configure回调里重复触发锁等待逻辑,自然绕开了死循环。
可行的解决方案
- 方案1:把所有在
setup_periodic_tasks回调里引用到的任务,统一从@shared_task更换为导入项目Celery实例的@app.task,从根源上避免回调触发时的二次绑定逻辑。 - 方案2:调整
proj/celery.py的初始化顺序,在绑定on_after_configure回调前,强制完成所有任务的绑定:# proj/celery.py 调整后示例 import os from celery import Celery os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings') app = Celery('proj') app.config_from_object('django.conf:settings', namespace='CELERY') app.autodiscover_tasks() # 手动触发finalize,提前完成所有shared_task的绑定 app.finalize() # 绑定定时任务回调 @app.on_after_configure.connect def setup_periodic_tasks(sender, **kwargs): sender.add_periodic_task( 3600, generate_weekly_report.s(), ) - 方案3:在定时任务回调里引用shared_task时,通过任务名字符串的方式调用,避免直接触发任务的绑定逻辑:
@app.on_after_configure.connect def setup_periodic_tasks(sender, **kwargs): sender.add_periodic_task( 3600, app.send_task('assetmanagementservice.tasks.generate_weekly_report').s(), )
内容的提问来源于stack exchange,提问作者Boburxon Aqoidxonov
相关产品推荐
相关产品推荐

