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

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

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

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

Any view can be declared async by making the callable part of it return a coroutine - commonly, this is done using async def. For a function-based view, this means declaring the whole view using async def. For a class-based view, this means declaring the HTTP method handlers, such as get() and post() as async def (not its __init__(), or as_view()).

Примечание

Django uses asgiref.sync.iscoroutinefunction to test if your view is asynchronous or not. If you implement your own method of returning a coroutine, ensure you use asgiref.sync.markcoroutinefunction so this function returns True.

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

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

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

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

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

Middleware can be built to support both sync and async contexts. Some of Django’s middleware is built like this, but not all. To see what middleware Django has to adapt for, you can turn on debug logging for the django.request logger and look for log messages about «Asynchronous handler adapted for middleware …».

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

If you want to call a part of Django that is still synchronous, you will need to wrap it in a sync_to_async() call. For example:

from asgiref.sync import sync_to_async

results = await sync_to_async(sync_function, thread_sensitive=True)(pk=123)

If you accidentally try to call a part of Django that is synchronous-only from an async view, you will trigger Django’s asynchronous safety protection to protect your data from corruption.

Запросы и ORM

New in Django Development version.

За некоторыми исключениями, 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()).

Django также поддерживает некоторые асинхронные методы модели, которые используют базу данных:

async def make_book(...):
    book = Book(...)
    await book.asave(using="secondary")

async def make_book_with_tags(tags, ...):
    book = await Book.objects.acreate(...)
    await book.tags.aset(tags)

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

Changed in Django 4.2:

Добавлена асинхронная модель и связанные с ней интерфейсы менеджера.

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

При работе в режиме, который не соответствует представлению (например, асинхронное представление в 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

If you’re in an environment other than IPython (or you can’t turn off autoawait in IPython for some reason), you are certain there is no chance of your code being run concurrently, and you absolutely need to run your sync code from an async context, then you can disable the warning by setting the DJANGO_ALLOW_ASYNC_UNSAFE environment variable to any value.

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

Если вы включите эту опцию и будете иметь одновременный доступ к 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 (the default): the sync function will run in the same thread as all other thread_sensitive functions. This will be the main thread, if the main thread is synchronous and you are using the async_to_sync() wrapper.
  • thread_sensitive=False: the sync function will run in a brand new thread which is then closed once the invocation completes.

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

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

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