Spec-Zone.ru › Django 6.0

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

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

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

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

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

Примечание

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

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

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

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

Декораторы

Следующие декораторы можно использовать как с синхронными, так и с асинхронными функциями-представлениями:

  • cache_control()
  • never_cache()
  • no_append_slash()
  • csp_override()
  • csp_report_only_override()
  • csrf_exempt()
  • csrf_protect()
  • ensure_csrf_cookie()
  • requires_csrf_token()
  • sensitive_variables()
  • sensitive_post_parameters()
  • gzip_page()
  • condition()
  • conditional_page()
  • etag()
  • last_modified()
  • require_http_methods()
  • require_GET()
  • require_POST()
  • require_safe()
  • vary_on_cookie()
  • vary_on_headers()
  • 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()

Подробные сведения приведены в разделе Асинхронные запросы. Кратко:

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

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)

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

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

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

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

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

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

Проведите собственное тестирование производительности, чтобы определить, как 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 классифицируются как «небезопасные для асинхронного режима» и защищены от выполнения в асинхронной среде. Основной пример — 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)

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

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, но при обновлении asgiref с предыдущей версии обязательно проверьте использование sync_to_async().

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

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

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

На практике это ограничение означает, что при вызове 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.

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

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

Spec-Zone.ru

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