Soporte asíncrono

Django cuenta con soporte para escribir vistas asíncronas («async»), junto con una pila de solicitudes completamente habilitada para async si estás ejecutando bajo ASGI. Las vistas asíncronas seguirán funcionando bajo WSGI, pero con penalizaciones en rendimiento y sin la capacidad de tener solicitudes largas que funcionen de manera eficiente.

Estamos aún trabajando en el soporte asíncrono para la ORM y otras partes de Django. Puedes esperar ver esto en futuras versiones. Por ahora, puedes utilizar el adaptador sync_to_async() para interactuar con las partes sincrónicas de Django. También hay una amplia gama de bibliotecas Python nativas asíncronas que puedes integrar.

Vistas asíncronas

Cualquier vista puede declararse como asíncrona haciendo que la parte callable de ella devuelva una coroutine - comúnmente, esto se hace utilizando async def. Para una vista basada en funciones, esto significa declarar toda la vista utilizando async def. Para una vista basada en clase, esto significa declarar los manejadores de métodos HTTP, como get() y post(), como async def (no su __init__() o as_view()).

Nota

Django utiliza asgiref.sync.iscoroutinefunction para comprobar si tu vista es asíncrona o no. Si implementas tu propio método de devolución de un coroutine, asegúrate de utilizar asgiref.sync.markcoroutinefunction para que esta función devuelva True.

Bajo un servidor WSGI, las vistas asíncronas ejecutarán su propio bucle de eventos uno a uno. Esto significa que puedes utilizar características asíncronas, como solicitudes HTTP asíncronas concurrentes, sin problemas, pero no obtendrás los beneficios de una pila asíncrona.

Los principales beneficios son la capacidad de atender a cientos de conexiones sin utilizar hilos de Python. Esto te permite usar transmisión en streaming lenta, peticionamiento largo y otros tipos de respuesta emocionantes.

Si deseas utilizar estos, necesitarás desplegar Django utilizando ASGI en su lugar.

Advertencia

Solo obtendrás los beneficios de una pila de solicitudes completamente asíncronas si no tienes ningún middleware sincrónico cargado en tu sitio. Si hay un pedazo de middleware sincrónico, entonces Django debe utilizar un hilo por solicitud para emular seguramente un entorno sincrónico para él.

El texto traducido es:

En ambos modos ASGI y WSGI, puedes seguir utilizando el soporte asíncrono de manera segura para ejecutar código de forma concurrente en lugar de seriada. Esto es especialmente útil cuando se trata con APIs o almacenes de datos externos.

Si deseas llamar a una parte de Django que todavía es sincrónica, necesitarás envolverla en un llamado a sync_to_async(). Por ejemplo:

from asgiref.sync import sync_to_async

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

Si intentas llamar accidentalmente una parte de Django que solo es sincrónica desde una vista asíncrona, activarás la protección contra seguridad asíncrona <async-safety> de Django para proteger tus datos de corrupción.

Decoradores

Los siguientes decoradores se pueden utilizar con funciones de vistas síncronas y asíncronas.

Por ejemplo:

from django.views.decorators.cache import never_cache


@never_cache
def my_sync_view(request): ...


@never_cache
async def my_async_view(request): ...

Queries y el ORM

Con algunas excepciones, Django puede ejecutar consultas ORM de manera asíncrona también:

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

Puedes encontrar notas detalladas en consultas-async, pero en resumen:

  • Todas las métodos QuerySet que causan una consulta SQL a ocurrir tienen un variante asíncrono con prefijo a.

  • Se admite async for en todos los QuerySets (incluyendo la salida de values() y values_list().)

Django también admite algunas métodos de modelo asíncronos que utilizan la base de datos:

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)

Las transacciones no funcionan aún en modo asíncrono. Si tienes un trozo de código que necesita comportamiento de transacción, te recomendamos escribir ese trozo como una función sincrónica individual y llamarla utilizando sync_to_async().

Conexiones persistentes a la base de datos , configuradas mediante el parámetro de configuración CONN_MAX_AGE, también deben estar deshabilitadas en modo asíncrono. En su lugar, utiliza la piscina de conexiones integrada de tu backend de base de datos si está disponible, o investiga una opción de piscina de conexiones de terceros si es necesario.

Rendimiento

Cuando se ejecuta en un modo que no coincide con la vista (por ejemplo, una vista asíncrona bajo WSGI, o una vista tradicional sincrónica bajo ASGI), Django debe emular el otro estilo de llamada para permitir que tu código se ejecute. Esta cambio de contexto causa una pequeña penalización en rendimiento de alrededor de un milisegundo.

This is also true of middleware. Django will attempt to minimize the number de cambios de contexto entre sincrónico y asíncrono. Si tienes un servidor ASGI, pero todos tus middleware y vistas son síncronos, se cambiará solo una vez, antes de que entre en el stack de middleware.

Sin embargo, si pones middleware síncrono entre un servidor ASGI y una vista asíncrona, tendrá que cambiar a modo sincrónico para el middleware y luego volver a modo asíncrono para la vista. Django también mantendrá el hilo de sincronía abierto para propagar excepciones de middleware. Esto puede no ser notorio al principio, pero agregar esta penalización de un hilo por solicitud puede eliminar cualquier ventaja de rendimiento asíncrona.

Debes realizar tus propias pruebas de rendimiento para ver qué efecto tiene ASGI versus WSGI en tu código. En algunos casos, puede haber un aumento de rendimiento incluso para una base de código puramente síncrona bajo ASGI porque el código de manejo de solicitudes sigue ejecutándose de manera asíncrona. En general, solo querrás habilitar modo ASGI si tienes código asíncrono en tu proyecto.

Gestión de desconexiones

Para solicitudes largas de vida, un cliente puede desconectarse antes de que la vista devuelva una respuesta. En este caso, se levantará un asyncio.CancelledError en la vista. Puedes atrapar este error y manejarlo si necesitas realizar cualquier limpieza:

async def my_view(request):
    try:
        # Do some work
        ...
    except asyncio.CancelledError:
        # Handle disconnect
        raise

También puedes manejar desconexiones de clientes en respuestas de streaming.

Seguridad asíncrona

DJANGO_ALLOW_ASYNC_UNSAFE

Ciertas partes clave de Django no pueden operar de manera segura en un entorno asíncrono, ya que tienen estado global que no es consciente de corutinas. Estas partes de Django se clasifican como «inseguras para ASGI» y están protegidas contra la ejecución en un entorno asíncrono. El ORM es el ejemplo principal, pero hay otras partes que también están protegidas de esta manera.

Si intentas ejecutar alguna de estas partes desde un hilo donde hay un evento loop en ejecución, obtendrás un error SynchronousOnlyOperation. Ten en cuenta que no tienes que estar dentro de una función asíncrona directamente para que ocurra este error. Si has llamado a una función síncrona directamente desde una función asíncrona, sin utilizar sync_to_async() o similar, entonces también puede ocurrir. Esto es porque tu código sigue ejecutándose en un hilo con un evento loop activo, aunque no esté declarado como código asíncrono.

Si encuentras este error, debes corregir tu código para que no llame al código ofendido desde un contexto asíncrono. En su lugar, escribe tu código que habla con funciones inseguras en sus propias funciones síncronas y llama a esa función utilizando asgiref.sync.sync_to_async() (o cualquier otra forma de ejecutar código síncrono en su propio hilo).

El async context puede imponerse sobre ti por el entorno en el que estás ejecutando tu código de Django. Por ejemplo, los cuadernos de Jupyter y las consolas interactivas IPython proporcionan transparentemente un bucle de eventos activo para que sea más fácil interactuar con APIs asíncronas.

Si estás utilizando una consola IPython, puedes deshabilitar este bucle de eventos ejecutando:

%autoawait off

como comando en la consola IPython. Esto te permitirá ejecutar código sincrónico sin generar errores SynchronousOnlyOperation; sin embargo, también no podrás await APIs asíncronas. Para volver a encender el bucle de eventos, ejecuta:

%autoawait on

Si estás en un entorno distinto de IPython (o no puedes deshabilitar autoawait en IPython por alguna razón), estás seguro de que no hay posibilidad de que tu código se ejecute de forma concurrente, y absolutamente necesitas ejecutar tu código sincrónico desde un contexto asíncrono, entonces puedes deshabilitar la advertencia estableciendo la variable de entorno DJANGO_ALLOW_ASYNC_UNSAFE a cualquier valor.

Advertencia

Si habilitas esta opción y hay acceso concurrente a las partes del async-unsafe de Django, puede sufrir pérdida o corrupción de datos. Sé muy cuidadoso y no utilices esto en entornos de producción.

Si necesitas hacer esto desde dentro de Python, hazlo con os.environ:

import os

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

Adaptadores de funciones asíncronas

Es necesario adaptar el estilo de llamada cuando llamar código sincrónico desde un contexto asíncrono o viceversa. Para ello hay dos funciones de adaptador, del módulo asgiref.sync: async_to_sync() y sync_to_async(). Se utilizan para transitar entre los estilos de llamada mientras se preserva la compatibilidad.

Estas funciones de adaptador se utilizan ampliamente en Django. El paquete asgiref mismo es parte del proyecto Django, y se instala automáticamente como dependencia cuando instalas Django con pip.

async_to_sync()

async_to_sync(async_function, force_new_loop=False)

Takes an async function and returns a sync function that wraps it. Can ser utilizado como un envoltorio directo o como decorador:

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

La función async se ejecuta en el bucle de eventos para el hilo actual, si está presente. Si no hay ningún bucle de eventos actual, se crea un nuevo bucle de eventos específicamente para la invocación async única y se cierra nuevamente una vez que se complete. En cualquier situación, la función async ejecutará en un hilo diferente al código llamante.

Los valores de threadlocals y contextvars se preservan a través del límite en ambas direcciones.

async_to_sync() esencialmente es una versión más poderosa de la función asyncio.run() en la biblioteca estándar de Python. Además de garantizar que funcionen los threadlocals, también habilita el modo thread_sensitive de sync_to_async() cuando se utiliza el envoltorio debajo de él.

sync_to_async()

sync_to_async(sync_function, thread_sensitive=True)

Takes a sync function and returns an async function that wraps it. Can be used as either a direct wrapper or a decorator:

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

Los valores de threadlocals y contextvars se preservan a través del límite en ambas direcciones.

Las funciones sincrónicas tienden a escribirse suponiendo que todas se ejecutan en el hilo principal, por lo que sync_to_async() tiene dos modos de hilos:

  • thread_sensitive=True (el valor predeterminado): la función sincrónica se ejecutará en el mismo hilo que todas las otras funciones thread_sensitive. Esto será el hilo principal, si el hilo principal es sincrónico y estás utilizando el envoltorio async_to_sync().

  • thread_sensitive=False: la función sincrónica se ejecutará en un nuevo hilo que luego se cerrará una vez que se complete la invocación.

Advertencia

La versión 3.3.0 de asgiref cambió el valor predeterminado del parámetro thread_sensitive a True. Este es un valor de seguridad más seguro, y en muchos casos interactuar con Django el valor correcto, pero asegúrate de evaluar las utilizaciones de sync_to_async() si actualizas asgiref desde una versión anterior.

El texto traducido es el siguiente:

La razón por la que se necesita en Django es que muchas bibliotecas, específicamente adaptadores de bases de datos, requieren que se acceda a ellas en el mismo hilo en el que fueron creadas. También hay mucho código existente de Django que asume que todo corre en el mismo hilo, por ejemplo, middleware que agregan cosas a la solicitud para su uso posterior en vistas.

En lugar de introducir potenciales problemas de compatibilidad con este código, optamos por agregar esta modalidad para que todo el código sincrónico existente de Django corra en el mismo hilo y así sea compatible completamente con el modo asíncrono. Ten en cuenta que el código sincrónico siempre estará en un hilo diferente a cualquier código asíncrono que lo llame, por lo que debes evitar pasar manejo de base de datos crudo o referencias sensibles a hilos alrededor.

En la práctica, esta restricción significa que no debes pasar características del objeto connection de la base de datos cuando llames a sync_to_async(). Hacerlo así activará las comprobaciones de seguridad de hilos:

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

En su lugar, debes encapsular todo el acceso a la base de datos dentro de una función auxiliar que se pueda llamar con sync_to_async() sin depender del objeto de conexión en el código llamante.