非同期サポート¶
Djangoは、ASGI の環境下であれば、完璧な非同期リクエストスタックに対応した非同期 ("async") ビューをサポートしています。WSGI の環境下でも非同期ビューは動作しますが、パフォーマンス上不利であるうえ、長期的なリクエストを効率的に処理できません。
開発チームは ORM や他の機能でも非同期処理が対応できるよう取り組んでいます。この機能は将来リリース予定ですが、現時点では、同期処理と非同期処理のやり取りに、sync_to_async()
アダプターが使えます。さらに同期処理と非同期処理を統合するために、非同期な Python ライブラリがすべて使えます。
非同期ビュー¶
どのようなビューでも、呼び出し可能オブジェクトでコルーチンを返すようにすれば、非同期として宣言できます。その際には、一般的に async def
を使います。関数ベースのビューの場合、async def
を用いてビュー全体を宣言します。クラスベースのビューの場合、get()
と post()
などのメソッドを async def
で宣言します (__init__()
でも as_view()
でもないことに注意)。
注釈
Django は、あなたのビューが非同期かどうか確かめるために asgiref.sync.iscoroutinefunction
を使います。もし、コルーチンを返すあなた独自のメソッドを実装する場合は、確実に asgiref.sync.markcoroutinefunction
を用いて asgiref.sync.iscoroutinefunction
が True
を返すようにして下さい。
WSGI サーバーでは、非同期ビューは1回限りのイベントループで実行されます。つまり、非同期 HTTP リクエストなどの非同期機能を問題なく使用できるものの、非同期スタックのメリットは得られないことになります。
非同期スタックの主な利点とは、数百もの接続を Python のスレッドを使わずに処理できることです。これにより、低速ストリーミング、ロングポーリング、その他の便利なレスポンスタイプが使えます。
もしこれらを利用したい場合は、代わりに ASGI を使って Django をデプロイする必要があります。
警告
同期ミドルウェアを使っていない場合にのみ、完全非同期のリクエストスタックの効果があります。もし、同期ミドルウェアがあれば、同期環境を安全にエミュレートするために、Django はリクエストごとにスレッドを使ってしまいます。
ミドルウェアは、同期と非同期の両方の コンテキストをサポートするように構築できます。 Djangoミドルウェアの一部はこのように構築されていますが、すべてではありません。ミドルウェアが適応する必要があるものを確認するには、django.request
ロガーのデバッグロギングを有効にして、"Asynchronous handler adapted for middleware ..." に関するログメッセージを探します。
ASGI モードと WSGI モードの両方で、コードを逐次的ではなく並行して実行するために、非同期サポートを安全に使用できます。これは、外部 API やデータストアを扱う際に特に便利です。
まだ同期的な Django の機能を呼び出したい場合、次のように sync_to_async()
でラップする必要があります。
from asgiref.sync import sync_to_async
results = await sync_to_async(sync_function, thread_sensitive=True)(pk=123)
非同期ビューから、同期的な Django の機能を誤って呼び出そうとすると、Django の 非同期セーフティプロテクション が発動して、データが破損しないように保護されます。
デコレータ¶
以下のデコレータは、同期ビュー関数でも非同期ビュー関数でも使用できます。
conditional_page()
xframe_options_deny()
xframe_options_sameorigin()
xframe_options_exempt()
例:
from django.views.decorators.cache import never_cache
@never_cache
def my_sync_view(request): ...
@never_cache
async def my_async_view(request): ...
クエリーと ORM¶
一部の例外を除いて、Django は ORM クエリーを非同期に実行することもできます。
async for author in Author.objects.filter(name__startswith="A"):
book = await author.books.afirst()
詳細については 非同期クエリ で参照できますが、簡単にまとめると次のようになります。
SQL クエリを発行するすべての
QuerySet
メソッドには、a
という接頭辞が付いた非同期版があります。async for
は (values()
とvalues_list()
を含む) すべての QuerySet でサポートされている
Django は、次のような非同期モデルのデータベースを使用するメソッドもいくつかサポートします。
async def make_book(*args, **kwargs):
book = Book(...)
await book.asave(using="secondary")
async def make_book_with_tags(tags, *args, **kwargs):
book = await Book.objects.acreate(...)
await book.tags.aset(tags)
トランザクションは非同期モードではまだ機能しません。もしトランザクションの動作が必要なコードがある場合は、そのコードを1つの同期的な関数として書いて、sync_to_async()
を使用して呼び出すことをおすすめします。
パフォーマンス¶
ビューと異なるモードで実行 (たとえば、非同期のビューを WSGI で実行、従来の同期ビューを ASGI で実行) した場合、Django はコードを実行するために、もう一方の呼び出しスタイルをエミュレートする必要があります。コンテキストスイッチにより、1 ms 程度の小さな性能上のペナルティが与えられてしまいます。
これはミドルウェアでも同様です。Django は同期・非同期の間のコンテキストスイッチの数を最小化するように試みます。もし ASGI サーバーがあっても、すべてのミドルウェアとビューが同期的だった場合、コンテキストスイッチは、サーバーがミドルウェアスタックに入る前の1回だけです。
しかし、同期ミドルウェアをASGIサーバーと非同期ビューの間においた場合、サーバーはミドルウェアのために同期モードに切り替え、ビューのために非同期モードにまた戻さなければならなくなります。Django はミドルウェアの例外の伝搬のために同期スレッドも保持し続けます。これは初めは気づかないほどの違いかもしれませんが、1リクエストごとに1スレッドのペナルティが与えられると、非同期の性能上の利点がすべて打ち消されてしまう可能性があります。
自分のコード上での ASGI と WSGI の違いの影響を知るためには、自分自身でパフォーマンステストを行うべきです。場合によっては、純粋に同期的なコードベースを ASGI 上で実行した場合でも、性能が向上することがあります。これは、それでもリクエストハンドリングのコードはすべて非同期に実行されるためです。一般的には、ASGI モードを有効にする必要があるのは、プロジェクトに非同期のコードがあるときだけです。
切断をハンドリングする¶
長時間のリクエストの場合、ビューが応答を返す前にクライアントが切断することがあります。この場合、asyncio.CancelledErrorがビューで発生します。このエラーをキャッチし、クリーンアップが必要な場合にハンドリングできます:
async def my_view(request):
try:
# Do some work
...
except asyncio.CancelledError:
# Handle disconnect
raise
また、ストリーミングレスポンスでクライアントの切断をハンドリングする こともできます。
非同期安全性¶
- DJANGO_ALLOW_ASYNC_UNSAFE¶
Django の特定の主要なパーツは、コルーチンが感知できないグローバルステートを持つため、非同期の環境では安全に操作できません。これらの Django のパーツは、"async-unsafe" として分類されており、非同期な環境での実行から保護されています。ORM は主な例ですが、この他にもこのように保護されているパーツがあります。
実行中のイベントループ があるスレッドからこれらのパーツを実行しようとすると、 SynchronousOnlyOperation
エラーが発生します。このエラーは、非同期関数の内部でなくても発生することに注意してください。 sync_to_async()
などを使わずに、非同期関数から直接同期関数を呼び出した場合にも発生します。これは、コードが同期コードとして宣言されていなくても、アクティブなイベントループを持つスレッドで実行されているためです。
このエラーが発生した場合は、問題のコードを非同期コンテキストから呼び出さないように修正する必要があります。代わりに、独自の同期関数それ自体の中で async-unsafe と通信するコードを書き、 asgiref.sync.sync_to_async()
(または、自スレッド内で同期コードを実行するための他の方法) を使って呼び出します。
非同期コンテキストは、Django コードを実行している環境によって与えられる場合もあります。たとえば、Jupyter notebooks や IPython 対話シェルはともにアクティブなイベントループを透過的に提供してくれるため、非同期 API との対話がより簡単になります。
もし IPython shell を使用している場合は、次のコマンドでこのイベントループを無効化できます。
%autoawait off
これは IPython のプロンプトのコマンドです。これにより同期的なコードを SynchronousOnlyOperation
エラーを起こさずに実行できるようになりますが、同時に、非同期 API を await
することもできなくなります。イベントループを戻すには、次のコマンドを実行します。
%autoawait on
IPython 以外の環境にいる場合 (または、何らかの理由で IPython で autoawait
がオフにできない場合)、コードが並行して実行される可能性は 確実に ないため、同期コードは 絶対に 非同期コンテキストから実行する必要があります。このとき、警告は DJANGO_ALLOW_ASYNC_UNSAFE
環境変数を任意の値に設定することで無効化できます。
警告
このオプションを有効にした上で、Djangoの async-unsafe パーツへ同時アクセスがあると、データが失われたり壊れたりする可能性があります。十分な注意を払い、本番環境では使用しないでください。
もし、これをPython内部から行いたい場合は、os.environ
を使用してください。
import os
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
非同期アダプター関数¶
同期コードを非同期コンテキストから呼び出したり、その逆をするためには、呼び出しスタイルを調整する必要があります。このために``asgiref.sync`` モジュールの async_to_sync()
と sync_to_async()
という2つのアダプター関数があります。これらは互換性を維持したまま呼び出しスタイル間を移行するために使われます。
これらのアダプター関数は Django で幅広く利用されています。asgiref パッケージ自体が Django プロジェクトの一部になっており、Django を pip
でインストールしたときに自動的に依存関係としてインストールされます。
async_to_sync()
¶
- async_to_sync(async_function, force_new_loop=False)¶
非同期関数を取り、それをラッピングした同期関数を返します。直接ラッパーとしてもデコレーターとしても使用できます。
from asgiref.sync import async_to_sync
async def get_data(): ...
sync_get_data = async_to_sync(get_data)
@async_to_sync
async def get_other_data(): ...
非同期関数は、もし存在すれば、現在のスレッドのイベントループの中で実行されます。現在のイベントループが存在しない場合、1つの非同期呼び出しのために専用の新しいイベントループが生成され、完了したら再び破棄されます。どちらの状況でも、非同期関数はコードが呼び出されたスレッドとは異なるスレッドで実行されます。
threadlocal とコンテキスト変数の値は双方向に境界を越えて保持されます。
async_to_sync()
は、Python 標準ライブラリの asyncio.run()
関数の強力なバージョンです。threadlocal が確実に機能するようにするだけでなく、それより下で sync_to_async()
のラッパーが使用されるときに、thread_sensitive
モードも有効にします。
sync_to_async()
¶
- sync_to_async(sync_function, thread_sensitive=True)¶
同期関数を取り、それをラッピングした非同期関数を返します。直接ラッパーとしてもデコレーターとしても使用できます。
from asgiref.sync import sync_to_async
async_function = sync_to_async(sync_function, thread_sensitive=False)
async_function = sync_to_async(sensitive_sync_function, thread_sensitive=True)
@sync_to_async
def sync_function(): ...
threadlocal とコンテキスト変数の値は双方向に境界を越えて保持されます。
同期関数はすべてメインスレッド内で実行されることを想定して書かれる傾向があるため、sync_to_async()
には2種類の threading モードがあります。
thread_sensitive=True
(デフォルト): 同期関数は他のすべてのthread_sensitive
関数と同じスレッド内で実行されます。もしメインスレッドが同期的でasync_to_sync()
ラッパーを使用している場合、このスレッドはメインスレッドになるでしょう。thread_sensitive=False
: 同期関数は、まったく新しいスレッドで実行されます。そのスレッドは、その後呼び出しが完了すると閉じられます。
警告
asgiref
バージョン 3.3.0 は thread_sensitive
のデフォルト値を True
に変更しました。これは安全側のデフォルトなので、多くの場合、正しい値で Django とインタラクションしますが、以前のバージョンから asgiref
をアップデートする場合は、必ず sync_to_async()
の使用を評価してください。
thread-sensitive モードは極めて特別なモードで、すべての関数が同一スレッドで実行されるようにするためにたくさんの仕事を行います。ただし、メインスレッドで正しく動作するように、スタック内での async_to_sync()
の使用に依存している ことに注意してください。もし asyncio.run()
や類似のメソッドを使用した場合、thread-sensitive な関数は単一の共有スレッドでの実行にフォールバックしますが、これはメインスレッドではありません。
Django でこれが必要な理由は、多くのライブラリ、特にデータベースアダプターが、自分が作られたのと同一スレッド内でのアクセスを必要とするためです。また、既存の多くの Django コードも、すべてが同一スレッドで実行されることを想定しています。たとえば、ビュー内で後で使用するためにリクエストに何かを追加するミドルウェアです。
このコードによって潜在的な互換性の問題を引き起こす代わりに、私たちはこのモードを追加する選択をしました。これにより、既存のすべての Django の同期コードが同一スレッドで実行されるようになり、したがって、非同期モードと完全に互換になります。同期コードは、それを呼び出すすべての非同期コードとは常に 異なる スレッド内にあるため、生のデータベースハンドルや、その他 thread-sensitive な参照を渡すことは避ける必要があることに注意してください。
実用的には、この制限は、sync_to_async()
を呼び出すときにデータベースの connection
オブジェクトの機能を渡してはならないことを意味します。もしそうした場合、スレッド安全性のチェックがトリガーされます:
# DJANGO_SETTINGS_MODULE=settings.py python -m asyncio
>>> import asyncio
>>> from asgiref.sync import sync_to_async
>>> from django.db import connection
>>> # In an async context so you cannot use the database directly:
>>> connection.cursor()
django.core.exceptions.SynchronousOnlyOperation: You cannot call this from
an async context - use a thread or sync_to_async.
>>> # Nor can you pass resolved connection attributes across threads:
>>> await sync_to_async(connection.cursor)()
django.db.utils.DatabaseError: DatabaseWrapper objects created in a thread
can only be used in that same thread. The object with alias 'default' was
created in thread id 4371465600 and this is thread id 6131478528.
代わりに、呼び出しコード内の connection オブジェクトに依存せずに、sync_to_async()
で呼び出すことができるヘルパー関数内にすべてのデータベースアクセスをカプセル化する必要があります。