Django从WSGI迁移到ASGI/Uvicorn时AppConfig.ready()同步调用引发的异步上下文问题
我完全理解你的困扰——把Django应用从WSGI切换到ASGI本想享受异步带来的性能提升,结果却卡在了AppConfig.ready()这个同步初始化环节,确实头疼!
问题的核心很明确:Django的AppConfig.ready()设计就是同步方法,但在ASGI模式下,应用启动时的上下文是异步的,直接在ready()里执行数据库操作会触发SynchronousOnlyOperation异常,因为Django禁止在异步上下文里直接调用同步的数据库操作。
别担心,咱们有几个可行的方案来解决这个问题,你可以根据自己的场景选择:
方案一:用sync_to_async结合线程执行异步初始化
因为ready()必须是同步方法,我们可以把缓存初始化逻辑包装到线程中,在线程内部用异步方式执行。这样既不违反ready()的同步要求,又能在ASGI上下文里安全地操作数据库。
修改你的my_app/apps.py代码:
from django.apps import AppConfig from asgiref.sync import sync_to_async import asyncio import threading from . import cached_dicts class MyAppConfig(AppConfig): name = 'my_app' def ready(self): # 定义异步版本的缓存初始化函数 async def async_init_cache(): # 用sync_to_async把同步函数包装成异步可调用对象 await sync_to_async(cached_dicts.set_up_cache_dicts)() # 定义线程执行逻辑 def run_async_task(): asyncio.run(async_init_cache()) # 启动守护线程执行初始化,避免阻塞应用启动 threading.Thread(target=run_async_task, daemon=True).start()
这里用守护线程是为了防止线程阻止应用正常关闭,同时确保缓存初始化在后台完成。如果你的应用启动后立刻需要缓存数据,可以考虑添加简单的等待逻辑,但一般启动阶段的初始化不会有这个问题。
方案二:自定义ASGI应用,利用生命周期事件初始化缓存
这个方案更贴合ASGI的设计思路——放弃在AppConfig.ready()里做初始化,转而在ASGI应用的启动阶段处理。ASGI规范定义了lifespan事件,允许我们在应用启动/关闭时执行自定义逻辑。
修改你的asgi.py文件:
import os import asyncio from django.core.asgi import get_asgi_application from asgiref.sync import sync_to_async from my_app.cached_dicts import set_up_cache_dicts os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'MyAppName.settings') async def application(scope, receive, send): # 处理ASGI生命周期事件 if scope["type"] == "lifespan": while True: message = await receive() if message["type"] == "lifespan.startup": # 异步执行缓存初始化 await sync_to_async(set_up_cache_dicts)() # 通知服务器启动完成 await send({"type": "lifespan.startup.complete"}) elif message["type"] == "lifespan.shutdown": await send({"type": "lifespan.shutdown.complete"}) return else: # 把HTTP/WS请求交给Django处理 django_app = get_asgi_application() await django_app(scope, receive, send)
这个方案完全避开了AppConfig.ready()的同步限制,所有初始化逻辑都在ASGI的异步上下文里执行,兼容性也很好——Uvicorn、Hypercorn等主流ASGI服务器都支持lifespan事件。
一些关键注意事项
- 千万别尝试把
AppConfig.ready()改成异步方法!Django内部是同步调用这个方法的,不会await它,这样做只会导致初始化代码被跳过,根本不会执行。 - 如果用线程方案,要注意线程安全:Django的ORM在多线程环境下是安全的,但如果你的缓存初始化涉及共享状态,要确保加锁或者用线程安全的数据结构。
- 不管用哪种方案,多worker进程模式下每个进程都会执行一次初始化,这和WSGI模式是一致的,不需要额外处理。
个人更推荐方案二,因为它更符合ASGI的设计理念,代码逻辑也更清晰,能从根源上避免同步/异步上下文的冲突。
内容来源于stack exchange

