Djangoで先にリスポンスを返してから重い処理を継続する簡単な方法を見つけたので紹介します。理解できていない部分も多いので、誤り等あればご指摘いただければ幸いです。
asyncioを使った非同期処理
Django 3.1より非同期処理的にリスポンスを返す非同期Viewが導入されました。例えば、標準ライブラリasyncioを使って次のような非同期Viewを書くと、スリープしている間に非同期的にリスポンスを返してくれます。
import asyncio
async def async_func():
for i in range(5):
await asyncio.sleep(1.0) # 1秒待機
print(i+1)
async def async_response(request):
task = asyncio.create_task(async_func())
task
return HttpResponse('Asyncronous Response')
上では、非同期関数async_func
を定義し、非同期Viewのasync_response
で呼び出しています。async_response
ではasyncio.create_task
を使って同時並行でコルーチンを実行しています。これをuvicornで実行してブラウザからアクセスすると以下のようなログが出ます("Project"はプロジェクト名で置き換えてください)。
$ uvicorn Project.asgi:application
INFO: Started server process [15308]
INFO: Waiting for application startup.
INFO: ASGI 'lifespan' protocol appears unsupported.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: 127.0.0.1:49470 - "GET /myapp/async_response HTTP/1.1" 200 OK
1
2
3
4
5
スリープの完了を待たずにResponseが返されているのが分かります。
sync_to_asyncによる非同期化
一方で、次のsync_func
のような同期関数に適応したい場合、コルーチンを返す非同期関数に変換する必要があります。ここで、ASGIのsync_to_async
を使うことができます。
import time, asyncio
from asgiref.sync import sync_to_async
def sync_func():
for i in range(5):
time.sleep(1.0) # 1秒待機
print(i+1)
async def async_response(request):
async_func2 = sync_to_async(sync_func, thread_sensitive=False)
task = asyncio.create_task(async_func2())
task
return HttpResponse('Asyncronous Response')
実行すると、先ほどと同様のログが出力されます。これにより、自分で定義した任意の処理を開始しつつ先にリスポンスを返すことができました。
以上を応用すると、バックエンド側で機械学習や数値計算などの重い処理を開始しつつ、リスポンスを先に返すといったことにも使えるかと思います。