Python Monthly Topics

Django非同期View入門

筒井@ryu22eです。8月の「Python Monthly Topics」は、Djangoでの非同期Viewの使い方について解説します。

Webアプリケーションの非同期処理とは何か?

非同期処理をサポートするWebアプリケーションでは、複数のリクエストを受け取った際、シングルスレッドの中で各リクエスト用の処理を細かく切り替えながら、同時に動かしているように見せかけて実行します(⁠⁠並行処理」とも言います⁠⁠。

非同期処理の利点として、Djangoの公式ドキュメントでは以下のように説明しています[1]

The main benefits are the ability to service hundreds of connections without using Python threads. This allows you to use slow streaming, long-polling, and other exciting response types.

つまり、非同期処理により、少ないリソースで大量のリクエストを効率よく捌くことができます。

Python製Webアプリケーションで非同期処理を実行するには

Webアプリケーションを動かすには、WebサーバーとPythonアプリケーションの間でデータのやり取りを仲介する標準インターフェースが必要です。Pythonの世界では、WSGI(Web Server Gateway Interface)という標準インターフェースがよく使われており、Djangoもこれをサポートしています。

ところが、WSGIは同期処理を前提とした仕様なので、非同期処理を実行するWebアプリケーションには対応できません。

そこで、WSGIの「Spiritual successor(精神的続編)[2]として誕生したのがASGI ⁠Asynchronous Server Gateway Interface)です。

ASGIはWSGIとの互換性を持ち、同期アプリケーション、非同期アプリケーションの両方をサポートします。

Djangoで非同期アプリケーションを作るには

Djangoは2019年12月2日にリリースされたバージョン3.0でASGIをサポートするようになりました。ところが、3.0時点では実際にアプリケーションを作成する際に必要な非同期Viewが提供されていないため、⁠ASGIをサポートしているのに非同期アプリケーションは作れない」という歯がゆい状況が続いていました。

そして、2021年3月にリリースされたバージョン3.1でようやく関数ベースViewを非同期に書くことが可能になりました。以下のように、関数の先頭にasyncを付けると非同期関数ベースViewになります[3]

import datetime
from django.http import HttpResponse

async def current_datetime(request):
    now = datetime.datetime.now()
    html = '<html><body>It is now %s.</body></html>' % now
    return HttpResponse(html)

しかし、バージョン3.1時点では非同期クラスベースViewは未サポートのため、クラスベース派の人たちにとっては依然として歯がゆさが残ります。

そして、今月その状況をさらに改善する大きなリリースがあります。2022年8月リリース予定のバージョン4.1では、ついに非同期クラスベースViewが書けるようになりました。

Django公式サイトの4.1に関するリリースノートについては以下を参照してください。

Django 4.1 release notes - UNDER DEVELOPMENT | Django Documentation
https://docs.djangoproject.com/en/dev/releases/4.1/

非同期クラスベースViewの書き方は簡単です。以下のように、メソッド定義の先頭にasyncを付けるだけです。

import asyncio

from django.http import HttpResponse
from django.views import View


class AsyncView(View):
    # メソッドの先頭にasyncを付ける
    async def get(self, request, *args, **kwargs):
        await asyncio.sleep(1)
        return HttpResponse("非同期ビューのレスポンス")

上記Viewは、1秒間待機した後「非同期ビューのレスポンス」という文字列をレスポンスとして返します。

asyncを付けなかった場合は、従来どおりの同期Viewとして動作します。

非同期Viewの中で呼んではいけない処理と、その回避方法

以下のコードを見てください。非同期Viewの中でModelを操作して、データベースの内容をレスポンスとして返しています。

from django.http import HttpResponse
from django.views import View

from .models import Book


class AsyncView(View):
    async def get(self, request, *args, **kwargs):
        titles = []
        for book in Book.objects.all():
            titles.append(book.title)
        return HttpResponse(",".join(titles))

一見問題ないコードですが、このViewにリクエストを送ってみると、以下のようにSynchronousOnlyOperationエラーが発生します。

SynchronousOnlyOperation
SynchronousOnlyOperation

SynchronousOnlyOperationエラーは、非同期処理の中で安全に呼べない処理が書かれていた場合に発生するエラーです。データベースの操作は同期専用の処理なので、このままでは非同期Viewの中に書くことができません。

上記コードを動かすには、以下のようにModelの操作にasyncを付けます。

from django.http import HttpResponse
from django.views import View

from .models import Book


class AsyncView(View):
    async def get(self, request, *args, **kwargs):
        titles = []
        async for book in Book.objects.all():
            titles.append(book.title)
        return HttpResponse(",".join(titles))

Model操作以外の同期専用の処理は、sync_to_async関数でラップすると非同期処理の中でも呼べるようになります。

from asgiref.sync import sync_to_async

def sync_only1():
   """同期処理専用の関数"""
   ...

# sync_to_asyncでラップして実行
await sync_to_async(sync_only1)()

# 関数デコレータとしても使える
@sync_to_async
def sync_only2():
   """同期処理専用の関数"""
   ...

SynchronousOnlyOperationエラーが発生するが、とりあえずコードを動かしてみたい場合は、環境変数DJANGO_ALLOW_ASYNC_UNSAFEに任意の値を設定することでエラーを回避できます。

ブラウザ上で動作確認するならDJANGO_ALLOW_ASYNC_UNSAFE=1 python manage.py runserver、テスト実行ならDJANGO_ALLOW_ASYNC_UNSAFE=1 python manage.py testとすれば、SynchronousOnlyOperationエラーを防ぐことができます。

ただし、DJANGO_ALLOW_ASYNC_UNSAFEは開発環境で一時的に動作確認する以外の用途で使わないでください。Djangoには非同期の実行を想定していない、グローバルな状態を保持する処理があり、SynchronousOnlyOperationエラーはそれらを非同期で実行させないための仕組みです。

SynchronousOnlyOperationを無視すると、想定外の挙動でデータの損失または破損が発生する可能性があるので注意してください。

Djangoで作った非同期アプリケーションを本番環境で動かすには

runserverコマンドはあくまで開発環境用の機能です。本番環境で動かすには、アプリケーションサーバーと組み合わせる必要があります。

非同期アプリケーションを本番環境で動かすには、ASGIに対応したアプリケーションサーバーが必要です。Python用アプリケーションサーバーといえばGunicornuWSGIなどが有名ですが、これらは ASGIに対応していません。非同期アプリケーションを動かすことはできますが、本来のパフォーマンスを発揮できません。

ASGIに対応したアプリケーションサーバーには以下のようなものがあります。

今回は、Uvicornを使ってアプリケーションを起動する例を紹介します。

Uvicornを使う方法はいくつかありますが[4]、今回はGunicornのワーカープロセスとして利用してみましょう。

まず、pipコマンドでUvicornとGunicornをインストールします。

$ pip install uvicorn gunicorn

gunicornコマンドの引数にASGI用設定が書かれたasgi.pyの場所を指定する必要があります。

asgi.pystartprojectコマンドでDjangoプロジェクトを作成した際に、プロジェクト名と同名のディレクトリの直下に作成されるファイルです。場所の指定は{プロジェクト名}.asgi:applicationのように書きます。-wオプションでワーカー数、-kオプションでワーカークラスを指定します。Uvicornが提供するワーカークラスはuvicorn.workers.UvicornWorkerです。

Djangoプロジェクト名がdjango41_exampleであるなら、gunicornコマンドには以下のような引数を渡します。

$ gunicorn django41_example.asgi:application -w 4 -k uvicorn.workers.UvicornWorker

アプリケーションが起動すると、http://127.0.0.1:8000が使えるようになります。

まとめ

Djangoの非同期Viewは多少気をつける点はあるものの、簡単に書けるし、間違っていても例外で教えてくれるので安心感がありますね。この記事を読んで興味を持った方は、ぜひ自分でもアプリケーションを作ってみてください!

おすすめ記事

記事・ニュース一覧