Поддержка асинхронного режима

Django поддерживает написание асинхронных («асинхронных») представлений, а также полностью асинхронный стек запросов, если вы работаете под ASGI. Асинхронные представления будут работать и под WSGI, но с потерями в производительности и без возможности иметь эффективные длительные запросы.

Мы все еще работаем над поддержкой async для ORM и других частей Django. Вы можете ожидать увидеть это в будущих релизах. Пока же вы можете использовать адаптер sync_to_async() для взаимодействия с синхронизируемыми частями Django. Существует также целый ряд async-нативных библиотек Python, с которыми вы можете интегрироваться.

Асинхронные представления

Любое представление можно объявить асинхронным, заставив вызываемую его часть возвращать coroutine - обычно это делается с помощью async def. Для представления, основанного на функциях, это означает объявление всего представления с помощью async def. Для представления на основе класса это означает объявление обработчиков методов HTTP, таких как get() и post(), как async def (не его __init__() или as_view()).

Примечание

Django использует asyncio.iscoroutinefunction для проверки того, является ли ваше представление асинхронным или нет. Если вы реализуете свой собственный метод возврата корутины, убедитесь, что вы установили атрибут _is_coroutine представления в asyncio.coroutines._is_coroutine, чтобы эта функция возвращала True.

В сервере WSGI асинхронные представления будут выполняться в собственном однократном цикле событий. Это означает, что вы можете использовать функции async, такие как одновременные async-запросы HTTP, без каких-либо проблем, но вы не получите преимуществ стека async.

Основные преимущества - возможность обслуживать сотни соединений без использования потоков Python. Это позволяет использовать медленные потоки, длинные опросы и другие интересные типы ответов.

Если вы хотите использовать их, вам нужно будет развернуть Django, используя ASGI вместо этого.

Предупреждение

Вы получите преимущества полностью асинхронного стека запросов только в том случае, если на вашем сайте нет никакого синхронного промежуточного ПО. Если есть синхронное промежуточное ПО, то Django должен использовать поток на запрос, чтобы безопасно эмулировать синхронную среду для него.

Среднее ПО может быть построено для поддержки контекстов both sync and async. Некоторые из промежуточных программ Django построены таким образом, но не все. Чтобы узнать, для какого промежуточного ПО Django приходится адаптироваться, вы можете включить отладочную регистрацию для логгера django.request и поискать в журнале сообщения о «Асинхронный обработчик адаптирован для промежуточного ПО … «.

В режиме 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, которая является только синхронной, из асинхронного представления, вы запустите asynchronous safety protection, чтобы защитить ваши данные от повреждения.

Запросы и ORM

New in Django 4.1.

За некоторыми исключениями, Django может выполнять ORM-запросы также асинхронно:

async for author in Author.objects.filter(name__startswith="A"):
    book = await author.books.afirst()

Подробные заметки можно найти в Асинхронные запросы, но вкратце:

  • All QuerySet methods that cause an SQL query to occur have an a-prefixed asynchronous variant.
  • async for поддерживается для всех наборов запросов (включая вывод values() и values_list()).

Транзакции пока не работают в асинхронном режиме. Если у вас есть часть кода, которой необходимо поведение транзакций, мы рекомендуем вам написать эту часть как одну синхронную функцию и вызывать ее с помощью sync_to_async().

Производительность

При работе в режиме, который не соответствует представлению (например, асинхронное представление в WSGI или традиционное синхронное представление в ASGI), Django должен эмулировать другой стиль вызова, чтобы ваш код мог работать. Это переключение контекста приводит к небольшому снижению производительности примерно на миллисекунду.

Это также относится к промежуточному программному обеспечению. Django попытается минимизировать количество переключений контекста между sync и async. Если у вас есть ASGI-сервер, но все ваши промежуточные программы и представления синхронны, то переключение произойдет только один раз, до того, как он войдет в стек промежуточных программ.

Однако, если вы поместите синхронное промежуточное ПО между ASGI-сервером и асинхронным представлением, ему придется переключиться в режим синхронизации для промежуточного ПО, а затем обратно в режим асинхронизации для представления. Django также будет держать поток синхронизации открытым для распространения исключений промежуточного ПО. Поначалу это может быть незаметно, но добавление штрафа в виде одного потока на запрос может свести на нет все преимущества производительности асинхронного режима.

Вы должны провести собственное тестирование производительности, чтобы увидеть, какое влияние ASGI по сравнению с WSGI оказывает на ваш код. В некоторых случаях даже для чисто синхронной кодовой базы в режиме ASGI может наблюдаться увеличение производительности, поскольку код обработки запросов все еще выполняется асинхронно. В общем, вы захотите включить режим ASGI, только если в вашем проекте есть асинхронный код.

Асинхронная безопасность

DJANGO_ALLOW_ASYNC_UNSAFE

Некоторые ключевые части Django не могут безопасно работать в асинхронном окружении, так как имеют глобальное состояние, не ориентированное на корутину. Эти части Django классифицируются как «async-unsafe» и защищены от выполнения в среде async. ORM является основным примером, но есть и другие части, которые также защищены таким образом.

Если вы попытаетесь запустить любую из этих частей из потока, где есть запущенный цикл событий, вы получите ошибку SynchronousOnlyOperation. Обратите внимание, что для возникновения этой ошибки не обязательно находиться непосредственно внутри асинхронной функции. Если вы вызвали функцию синхронизации непосредственно из асинхронной функции, не используя sync_to_async() и т.п., то это также может произойти. Это происходит потому, что ваш код все еще выполняется в потоке с активным циклом событий, даже если он не объявлен как async-код.

Если вы столкнулись с этой ошибкой, вам следует исправить свой код, чтобы не вызывать небезопасный код из контекста async. Вместо этого напишите код, который обращается к async-небезопасным функциям, в собственной функции синхронизации и вызовите ее, используя asgiref.sync.sync_to_async() (или любой другой способ запуска кода синхронизации в собственном потоке).

Асинхронный контекст может быть навязан вам средой, в которой вы выполняете свой код Django. Например, блокноты Jupyter и интерактивные оболочки IPython прозрачно предоставляют активный цикл событий, чтобы было проще взаимодействовать с асинхронными API.

Если вы используете оболочку IPython, вы можете отключить этот цикл событий, выполнив:

%autoawait off

в качестве команды в приглашении IPython. Это позволит вам выполнять синхронный код без генерации SynchronousOnlyOperation ошибок; однако вы также не сможете await асинхронные API. Чтобы снова включить цикл событий, выполните:

%autoawait on

Если вы находитесь в среде, отличной от IPython (или по каким-то причинам не можете отключить autoawait в IPython), вы уверены в отсутствии вероятности одновременного выполнения вашего кода, и вам абсолютно необходимо запустить ваш синхронный код из асинхронного контекста, то вы можете отключить предупреждение, установив переменную окружения DJANGO_ALLOW_ASYNC_UNSAFE в любое значение.

Предупреждение

Если вы включите эту опцию и будете иметь одновременный доступ к async-небезопасным частям Django, вы можете пострадать от потери или повреждения данных. Будьте очень осторожны и не используйте эту опцию в производственных средах.

Если вам нужно сделать это из Python, сделайте это с помощью os.environ:

import os

os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

Функции асинхронного адаптера

Необходимо адаптировать стиль вызова при вызове синхронного кода из асинхронного контекста или наоборот. Для этого существуют две функции-адаптера из модуля asgiref.sync: async_to_sync() и sync_to_async(). Они используются для перехода между стилями вызова с сохранением совместимости.

Эти функции адаптера широко используются в 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(...):
    ...

Функция async выполняется в цикле событий для текущего потока, если он существует. Если текущего цикла событий нет, создается новый цикл событий специально для единственного вызова async-функции и снова закрывается после его завершения. В любом случае функция async будет выполняться в потоке, отличном от вызывающего кода.

Значения Threadlocals и contextvars сохраняются через границу в обоих направлениях.

async_to_sync() - это, по сути, более мощная версия функции asyncio.run() из стандартной библиотеки Python. Помимо обеспечения работы потоковых локалей, она также включает режим thread_sensitive в sync_to_async(), когда эта обертка используется под ней.

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(...):
    ...

Значения Threadlocals и contextvars сохраняются через границу в обоих направлениях.

Функции синхронизации обычно пишутся с учетом того, что все они выполняются в главном потоке, поэтому sync_to_async() имеет два режима работы с потоками:

  • thread_sensitive=True (по умолчанию): функция синхронизации будет выполняться в том же потоке, что и все остальные функции thread_sensitive. Это будет основной поток, если основной поток синхронный и вы используете обертку async_to_sync().
  • thread_sensitive=False: функция синхронизации будет запущена в совершенно новом потоке, который затем будет закрыт после завершения вызова.

Предупреждение

asgiref версии 3.3.0 изменил значение параметра по умолчанию thread_sensitive на True. Это более безопасное значение по умолчанию, и во многих случаях взаимодействия с Django это правильное значение, но не забудьте оценить использование sync_to_async() при обновлении asgiref с предыдущей версии.

Режим, чувствительный к потоку, довольно специфичен и выполняет много работы для запуска всех функций в одном потоке. Обратите внимание, однако, что он зависит от использования async_to_sync() выше него в стеке для правильного выполнения функций в главном потоке. Если вы используете asyncio.run() или подобное, он вернется к выполнению чувствительных к потоку функций в одном общем потоке, но это не будет основной поток.

В Django это необходимо потому, что многие библиотеки, в частности адаптеры баз данных, требуют, чтобы доступ к ним осуществлялся в том же потоке, в котором они были созданы. Также много существующего кода Django предполагает, что все выполняется в одном потоке, например, промежуточное ПО, добавляющее что-то в запрос для последующего использования в представлениях.

Вместо того, чтобы создавать потенциальные проблемы совместимости с этим кодом, мы решили добавить этот режим, чтобы весь существующий код синхронизации Django выполнялся в одном потоке и, таким образом, был полностью совместим с режимом async. Обратите внимание, что код синхронизации всегда будет находиться в отличном потоке от вызывающего его кода async, поэтому вам следует избегать передачи необработанных дескрипторов баз данных или других чувствительных к потоку ссылок.

На практике это ограничение означает, что вы не должны передавать характеристики объекта базы данных connection при вызове sync_to_async(). Это приведет к срабатыванию проверок безопасности потоков:

# 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.

Скорее, вы должны инкапсулировать весь доступ к базе данных в вспомогательную функцию, которую можно вызвать с помощью sync_to_async(), не полагаясь на объект соединения в вызывающем коде.

Вернуться на верх