Spec-Zone.ru › Django 4.1

Асинхронная поддержка

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

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

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

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

Примечание

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

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

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

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

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

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

Middleware можно разработать для поддержки синхронных и асинхронных контекстов. Некоторые middleware Django разработаны таким образом, но не все. Чтобы увидеть, какой middleware Django необходимо адаптировать, можно включить отладовую регистрацию для логгера django.request и искать сообщения логов о «Асинхронный обработчик адаптирован для 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 сработает защитой от асинхронных ошибок, чтобы защитить ваши данные от повреждения.

Запросы и ORM

Новое в Django 4.1.

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

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

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

  • Все QuerySet методы, которые вызывают SQL-запрос, имеют асинхронный вариант с префиксом a.
  • async for поддерживается для всех QuerySet (включая вывод values() и values_list()).

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

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

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

Это также верно для middleware. Django попытается минимизировать количество изменений контекста между синхронным и асинхронным режимом. Если у вас сервер ASGI, но все middleware и представления синхронны, он изменит контекст один раз, прежде чем войдёт в стек middleware.

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

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

Безопасность асинхронного режима

DJANGO_ALLOW_ASYNC_UNSAFE

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

Если вы попытаетесь запустить какую-либо из этих частей из потока, в котором есть запущенный цикл событий, вы получите ошибку SynchronousOnlyOperation. Обратите внимание, что вам не обязательно быть внутри асинхронной функции непосредственно, чтобы получить эту ошибку. Если вы вызвали синхронную функцию непосредственно из асинхронной функции, не используя sync_to_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 в любое значение.

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

Если вы включите этот параметр и будет одновременный доступ к небезопасным для асинхронного режима частям 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(...):
    ...

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

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

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

sync_to_async()

sync_to_async(sync_function, thread_sensitive=True) [source]

Принимает синхронную функцию и возвращает асинхронную функцию, которая её обёртывает. Может быть использована как непосредственный обёртный метод или как декоратор:

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 предполагают, что всё выполняется в одном потоке, например, middleware добавляет вещи в запрос для последующего использования в представлениях.

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

На практике это ограничение означает, что вы не должны передавать характеристики объекта базы данных 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() без зависимости от объекта подключения в вызывающем коде.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/4.1/topics/async/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API