Gestion du code asynchrone

Django prend en charge l’écriture de vues asynchrones (« async »), en conjonction avec une pile de requête adaptée pour l’asynchrone, pour autant que vous fonctionniez avec ASGI. Les vues asynchrones fonctionnent aussi avec WSGI, mais avec des performances moindres et sans possibilité de bénéficier de requêtes de longue durée efficaces.

Le travail sur la prise en charge de l’asynchrone est toujours en cours sur l’ORM et d’autres parties de Django. Nous espérons en voir le fruit dans de futures publications de Django. Pour le moment, vous pouvez utiliser l’adaptateur sync_to_async() pour interagir avec les parties synchrones de Django. Il existe aussi un vaste choix de bibliothèques Python nativement asynchrones avec lesquelles vous pouvez intégrer votre code.

Vues asynchrones

Toute vue peut être déclarée asynchrone en faisant renvoyer une coroutine de sa partie exécutable ; ceci se fait en principe avec async def. Pour une vue basée sur une fonction, cela signifie déclarer la vue entière comme async def. Pour une vue basée sur une classe, cela signifie que sa méthode __call__() doit être définie avec async def (pas la méthode __init__() ni as_view()).

Note

Django utilise asyncio.iscoroutinefunction pour tester si la vue est asynchrone ou pas. Si vous implémentez votre propre méthode pour renvoyer une coroutine, prenez soin de définir l’attribut _is_coroutine de la vue à asyncio.coroutines._is_coroutine pour que cette fonction renvoie True.

Avec un serveur WSGI, les vues asynchrones tournent dans leur propre boucle événementielle unique. Cela veut dire que vous pouvez utiliser sans problème des fonctionnalités asynchrones, telles que des requêtes HTTP asynchrones et concurrentes, mais vous ne bénéficierez pas des avantages d’une pile asynchrone.

Les bénéfices principaux sont de pouvoir servir des centaines de connexions sans faire appel aux files d’exécutions (threads) Python. Cela vous permet d’utiliser les flux lents et les interrogations lentes (slow streaming, long-polling), et autres techniques semblables pour les réponses.

Si vous souhaitez exploiter ces possibilités, vous devrez déployer Django en utilisant plutôt un serveur ASGI.

Avertissement

Vous ne pourrez obtenir les bénéfices d’une pile de requête pleinement asynchrone que si aucun intergiciel synchrone n’est chargé dans votre site. Si un intergiciel synchrone est chargé, Django est obligé d’employer un fil d’exécution par requête pour émuler un environnement synchrone de manière sûre.

Un intergiciel peut être conçu pour prendre en charge à la fois des contextes synchrones et asynchrones. Certains intergiciels de Django sont conçus de cette manière, mais pas tous. Pour voir quels sont les intergiciels que Django doit adapter, vous pouvez activer la journalisation de débogage pour le journaliseur django.request et observer les messages mentionnant « Synchronous middleware … adapted ».

Que ce soit en mode ASGI ou WSGI, vous pouvez toujours exploiter la prise en charge asynchrone de manière sûre pour exécuter du code de manière concurrente plutôt qu’en série. C’est particulièrement pratique lorsqu’on interagit avec des API externes ou des stockages de données.

Si vous souhaitez appeler une partie de Django qui est encore synchrone, comme l’ORM, il vous faut l’envelopper dans un appel à sync_to_async(). Par exemple

from asgiref.sync import sync_to_async

results = await sync_to_async(Blog.objects.get, thread_sensitive=True)(pk=123)

Il peut être plus simple de déplacer le code lié à l’ORM dans sa propre fonction et d’appeler cette fonction en utilisant sync_to_async(). Par exemple

from asgiref.sync import sync_to_async

def _get_blog(pk):
    return Blog.objects.select_related('author').get(pk=pk)

get_blog = sync_to_async(_get_blog, thread_sensitive=True)

Si par accident vous essayez d’appeler une partie de Django qui est encore purement synchrone à partir d’une vue asynchrone, vous déclencherez la protection de sécurité asynchrone de Django qui vise à protéger vos données d’une éventuelle corruption.

Performance

Lorsque vous fonctionnez dans un mode qui ne correspond pas à celui de la vue (par ex. une vue asynchrone avec WSGI ou une vue synchrone traditionnelle avec ASGI), Django doit émuler l’autre style d’appel pour permettre au code de s’exécuter. Cette bascule de contexte provoque une petite pénalité de performance d’environ une milliseconde.

Ceci vaut aussi pour les intergiciels. Django essaie de minimiser le nombre de bascules de contexte entre code synchrone et asynchrone. Si vous fonctionnez avec un serveur ASGI mais que tous vos intergiciels et vues sont synchrones, la bascule ne se fait qu’une fois, avant d’entrer dans la pile des intergiciels.

Cependant, si vous placez un intergiciel synchrone entre un serveur ASGI et une vue asynchrone, Django devra basculer en mode synchrone pour l’intergiciel puis revenir en mode asynchrone pour la vue. Il devra aussi laisser le fil d’exécution synchrone ouvert pour la propagation des exceptions d’intergiciel. Cela peut être d’abord à peine perceptible, mais cet handicap d’un fil d’exécution par requête peut potentiellement supprimer les avantages de la performance asynchrone.

Nous suggérons de faire vos propres tests de performance pour observer les différences entre ASGI et WSGI avec votre code. Dans certains cas, les performances peuvent être meilleures avec ASGI même pour une base de code purement synchrone car le code de traitement des requêtes fonctionne toujours de manière asynchrone. Mais en général, l’activation du mode ASGI n’est profitable que si votre code contient du code asynchrone.

Isolation de code asynchrone

DJANGO_ALLOW_ASYNC_UNSAFE

Certaines parties essentielles de Django ne sont pas capables d’opération de manière sûre dans un environnement asynchrone, car elles se basent sur un état global qui n’est pas compatible avec les coroutines. Ces parties de Django sont classées comme non sûres pour l’asynchrone (« async-unsafe ») et sont protégées contre l’exécution dans un environnement asynchrone. L’exemple principal est l’ORM (dialogue avec les bases de données), mais d’autres parties sont protégées de la même manière.

Si vous essayez d’exécuter l’une de ces parties depuis un fil d’exécution où une boucle événementielle s’exécute, vous obtiendrez une erreur SynchronousOnlyOperation. Notez que vous n’avez pas besoin d’être directement dans une fonction asynchrone pour déclencher cette erreur. Si vous avez appelé une fonction synchrone directement depuis une fonction asynchrone sans utiliser sync_to_async() ou un équivalent, cela peut alors aussi arriver, car votre code se trouve encore dans un fil d’exécution avec une boucle événementielle active, même s’il n’est pas déclaré comme code asynchrone.

Si vous obtenez cette erreur, vous devriez corriger votre code pour qu’il n’appelle pas le code de manière fautive depuis un contexte asynchrone. Au lieu de cela, écrivez le code communiquant avec des fonctions non adaptées à l’asynchrone dans sa propre fonction synchrone et en l’appelant par asgiref.sync.sync_to_async() (ou toute autre méthode d’exécution de code synchrone dans son propre fil d’exécution).

Le contexte asynchrone peut vous être imposé par l’environnement dans lequel s’exécute le code Django. Par exemple, les carnets Jupyter et les shells interactifs IPython fournissent tous deux de manière transparente une boucle évènementielle active pour qu’il soit plus facile d’interagir avec des API asynchrones.

Si vous utilisez un shell IPython, vous pouvez désactiver cette boucle évènementielle en exécutant

%autoawait off

comme commande à l’invite IPython. Cela permet d’exécuter du code synchrone sans que des erreurs SynchronousOnlyOperation se produisent ; cependant, vous ne serez pas non plus capable d’appeler des API asynchrones par await. Pour réactiver la boucle évènementielle, exécutez

%autoawait on

Si vous vous trouvez dans un environnement autre que IPython (ou que vous ne pouvez pas désactiver autoawait dans IPython pour une raison quelconque), que vous êtes certain qu’il n’y a aucun risque que le code soit lancé de manière concurrente et que vous avez absolument besoin de lancer ce code synchrone à partir d’un contexte asynchrone, vous pouvez alors désactiver l’avertissement en définissant la variable d’environnement DJANGO_ALLOW_ASYNC_UNSAFE à une valeur quelconque.

Avertissement

Si vous activez cette option et qu’un accès concurrent se produit sur des parties de Django non adaptées à l’asynchrone, vous pourriez expérimenter des pertes ou des corruptions de données. Soyez très prudent et n’utilisez pas cela dans des environnements de production.

Si vous devez faire cela depuis du code Python, faites-le avec os.environ:

import os

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

Fonctions d’adaptation asynchrone

Il est nécessaire d’adapter le style d’appel lors des appels de code synchrone depuis un contexte asynchrone ou vice versa. Il existe pour cela deux fonctions d’adaptation dans le module asgiref.sync: async_to_sync() et sync_to_async(). Elles sont utiles pour faire le passage entre les styles d’appel tout en préservant la compatibilité.

Ces fonctions d’adaptation sont largement utilisées dans Django. Le paquet asgiref lui-même fait partie du projet Django et il est automatiquement installé comme dépendance lorsqu’on installe Django avec pip.

async_to_sync()

async_to_sync(async_function, force_new_loop=False)

Accepte une fonction asynchrone et renvoie une fonction synchrone qui l’enveloppe. Peut être utilisée sous forme directe ou comme décorateur

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 fonction asynchrone est exécutée dans la boucle événementielle du fil d’exécution actuel, le cas échéant. S’il n’y a pas de boucle événementielle, une nouvelle boucle est générée spécifiquement pour cette invocation asynchrone unique et arrêtée dès que la fonction est terminée. Quelle que soit la situation, la fonction asynchrone s’exécutera dans un fil d’exécution différent de celui du code appelant.

Les valeurs « threadlocals » et « contextvars » sont préservées de part et d’autres des exécutions.

async_to_sync() est fondamentalement une version plus puissante de la fonction asyncio.run() de la bibliothèque Python standard. En plus de s’assurer du fonctionnement de « threadlocals », elle active aussi le mode thread_sensitive de sync_to_async() lorsque cette dernière est utilisée en dessous d’elle.

sync_to_async()

sync_to_async(sync_function, thread_sensitive=True)[source]

Accepte une fonction synchrone et renvoie une fonction asynchrone qui l’enveloppe. Peut être utilisée sous forme directe ou comme décorateur

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

Les valeurs « threadlocals » et « contextvars » sont préservées de part et d’autres des exécutions.

Les fonctions synchrones ont tendances à être écrites en partant du principe qu’elles s’exécutent toutes dans le fil d’exécution principal, ce qui fait que sync_to_async() dispose de deux modes d’exécution :

  • thread_sensitive=True (valeur par défaut) : la fonction synchrone sera exécutée dans le même fil d’exécution que toutes les autres fonctions thread_sensitive. Ce fil sera le fil principal si celui-ci est synchrone et que vous utilisez la fonction enveloppeuse async_to_sync().
  • thread_sensitive=False: la fonction synchrone sera exécutée dans un tout nouveau fil d’exécution qui sera ensuite détruit lorsque l’invocation sera terminée.

Avertissement

La version 3.3.0 de asgiref a modifié la valeur par défaut du paramètre thread_sensitive à True. Il s’agit d’une valeur par défaut plus sûre et la bonne valeur dans de nombreuses situations d’interaction avec Django, mais prenez soin d’évaluer les utilisations de sync_to_async() si vous mettez à jour asgiref à partir d’une version plus ancienne.

Le mode « thread-sensitive » est très spécial et fait beaucoup d’efforts pour exécuter toutes les fonctions dans le même fil d’exécution. Notez toutefois qu’il compte sur l’utilisation de async_to_sync() au-dessus de lui dans la pile d’appels pour lancer les choses correctement dans le fil d’exécution principal. Si vous utilisez asyncio.run() ou une méthode semblable, il se limite à exécuter les fonctions dépendantes du fil d’exécution dans un seul fil partagé, mais il ne s’agira pas du fil d’exécution principal.

La raison de sa présence dans Django est que de nombreuses bibliothèques, particulièrement les adaptateurs de base de données, exigent que leur accès se fasse dans le même fil d’exécution dans lequel elles ont été créées. De même, une grande quantité de code existant dans Django présuppose qu’il s’exécute entièrement dans le même fil d’exécution, par ex. un intergiciel ajoutant des éléments à une requête pour utilisation ultérieure dans les vues.

Plutôt que d’introduire des problèmes potentiels de compatibilité avec ce code, nous avons choisi d’ajouter ce mode afin que tout code synchrone existant dans Django soit exécuté dans le même fil d’exécution et reste donc pleinement compatible avec le mode asynchrone. Notez que le code synchrone sera toujours exécuté dans un fil d’exécution différent du code asynchrone appelant, ce qui implique que vous devez éviter de passer des pointeurs bruts de base de données ou d’autres références sensibles au fil d’exécution.

En pratique, cette restriction signifie que vous ne devriez pas transmettre des fonctionnalités de l’objet de base de données connection lors de l’appel à sync_to_async(). Si vous le faites, cela déclenchera les contrôles de sécurité des fils d’exécution :

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

Au lieu de cela, vous devriez encapsuler tous les accès à la base de données dans une fonction utilitaire pouvant être appelée par sync_to_async() sans vous baser sur l’objet de connexion dans le code appelant.

Back to Top