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 un léger coût d’adaptation pour chaque requête (voir Performance) et sans possibilité de bénéficier de requêtes de longue durée efficaces.
De nombreuses parties de Django fournissent des API asynchrones, dont l’ORM, l’infrastructure de cache, l’authentification, les sessions et les signaux. Pour le reste du code, l’adaptateur sync_to_async() est une passerelle à faible coût (voir Performance). Une large gamme de bibliothèques Python nativement asynchrones peuvent aussi être intégrées.
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 définir les gestionnaires de méthodes tels que get() et post() comme async def (pas la méthode __init__() ni as_view()).
Note
Django utilise inspect.iscoroutinefunction pour tester si la vue est asynchrone ou pas. Si vous implémentez votre propre méthode pour renvoyer une coroutine, prenez soin d’utiliser inspect.markcoroutinefunction 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.
Note
Une pile de requêtes entièrement asynchrone exige un intergiciel asynchrone de bout en bout. Lorsqu’un élément d’intergiciel synchrone se situe entre un serveur ASGI et une vue asynchrone, Django l’adapte en l’exécutant dans son propre fil ; voir Performance pour le compromis de performance.
Les intergiciels livrés par Django prennent en charge aussi bien le synchrone que l’asynchrone. Les intergiciels tiers ne le font pas forcément. Pour voir quels sont les intergiciels que Django adapte, activez la journalisation de débogage pour le journaliseur django.request et observer les messages mentionnant « Asynchronous handler adapted for middleware… ».
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, 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(sync_function, thread_sensitive=True)(pk=123)
Si par accident vous essayez d’appeler une partie de Django qui est 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.
Décorateurs¶
Les décorateurs suivants peuvent être utilisés avec des fonctions de vues à la fois synchrones et asynchrones :
conditional_page()xframe_options_deny()xframe_options_sameorigin()xframe_options_exempt()
Par exemple :
from django.views.decorators.cache import never_cache
@never_cache
def my_sync_view(request): ...
@never_cache
async def my_async_view(request): ...
method_decorator() peut être utilisé avec des méthodes asynchrones, y compris avec des gestionnaires de vues async def. Notez cependant que si vous décorez dispatch() sur une View avec des gestionnaires asynchrones, vous devrez surcharger dispatch pour la rendre aussi async def:
class MyClass(View):
@method_decorator(never_cache)
async def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
async def get(self, request): ...
async def post(self, request): ...
Ici, comme nous décorons dispatch plutôt que des méthodes de gestionnaire individuel, nous devons rendre dispatch asynchrone afin que method_decorator puisse marquer correctement la méthode résultante comme fonction coroutine.
Requêtes et ORM¶
À part quelques exceptions, Django peut lancer des requêtes d’ORM de manière asynchrone :
async for author in Author.objects.filter(name__startswith="A"):
book = await author.books.afirst()
Des notes détaillées peuvent être consultées dans Requêtes asynchrones, mais en résumé :
Toutes les méthodes
QuerySetgénérant une requête SQL possèdent une variante asynchrone dotée du préfixea.async forest pris en charge pour tous les QuerySets (y compris les résultats devalues()etvalues_list()).
Les méthodes de modèles asynchrones utilisant la base de données sont également prises en charge :
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)
Les transactions ne fonctionnent pas encore en mode asynchrone. Si vous écrivez un bout de code nécessitant un comportement transactionnel, nous vous recommandons de l’écrire sous forme d’une seule fonction synchrone et de l’appeler avec sync_to_async().
Les connexions persistantes à la base de données définies par le réglage CONN_MAX_AGE, devraient aussi être désactivées en mode asynchrone. À la place, nous suggérons d’utiliser le système de pool de connexions de votre moteur de base de données, si disponible, ou recherchez un système de tierce partie si nécessaire. Comme pour Django en mode synchrone, les requêtes concurrentes dans un même processus partagent ce pool, il s’agit donc de le dimensionner en fonction de la concurrence de requêtes ciblée en production.
Performance¶
En fonctionnement dans un mode ne correspondant pas à celui de la vue (par ex. une vue asynchrone sous WSGI ou une vue synchrone traditionnelle sous ASGI), Django doit émuler l’autre style d’appel pour permettre au code de fonctionner. Le coût par appel de cette adaptation est faible : quelques dizaines de microsecondes dans le chemin ASGI en requête, lorsque la boucle événementielle existante est réutilisée, et quelques centaines de microsecondes dans le chemin « à froid » utilisé par les commandes d’administration, les tâches d’arrière-plan et les scripts. En comparaison des temps de requête typiques mesurés en millisecondes, ces temps sont rarement perceptibles, mais peuvent le devenir sous contention GIL (interpréteur global) car le nombre de fils d’exécution actifs augmente.
Si vous vous retrouvez à envelopper des lignes ou opérations dans une boucle serrée, restructurez votre code pour que la boucle fonctionne dans une seule opération sync_to_async() (ou async_to_sync()). Le coût par appel de la bascule de contexte est alors partagé par la boucle entière et disparaît pratiquement.
Ce même coût d’adaptation par appel 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. Pour les vues requête/réponse qui sollicitent l’ORM, puis se terminent, la pénalité est en principe négligeable. Cela est par contre plus conséquent en mode ASGI pour des concurrences élevées dans un processus pour des entrées/sorties non liées à l’ORM (par exemple des multi-requêtes HTTP amont (fan-out), les événements envoyés par les serveurs (SSE) ou les requêtes longue durée), où le fil d’exécution supplémentaire par requête atteint les limites de concurrence.
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.
Gestion des déconnexions¶
Pour les requêtes de longue durée, un client peut se déconnecter avant que la vue renvoie une réponse. Dans ce cas, asyncio.CancelledError sera générée dans la vue. Vous pouvez intercepter cette erreur et la traiter si vous avez besoin d’effectuer un quelconque nettoyage :
async def my_view(request):
try:
# Do some work
...
except asyncio.CancelledError:
# Handle disconnect
raise
Vous pouvez aussi traiter les déconnexions de client dans les réponses en flux.
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’API synchrone de l’ORM en est l’exemple principal, 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. À froid (sans boucle événementielle déjà lancée), elle paie le coût du démarrage d’une nouvelle boucle événementielle, comme pour asyncio.run(); lorsqu’une telle boucle existe déjà (le cas dans une requête ASGI), la boucle est réutilisée et le coût s’allège donc d’autant.
sync_to_async()¶
- sync_to_async(sync_function, thread_sensitive=True)¶
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 fonctionsthread_sensitive. Ce fil sera le fil principal si celui-ci est synchrone et que vous utilisez la fonction enveloppeuseasync_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.
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.
Dans une même requête, plusieurs appels thread_sensitive sont sérialisés sur le fil d’exécution actif de la requête, mais chaque requête reçoit sont propre fil par contexte, ce qui fait que les requêtes concurrentes ne sont pas sérialisées entre elles. Cela imite le modèle Django d’une « connexion par fil d’exécution », et la même contrainte s’applique dans d’autres bibliothèques de base de données asynchrones, où les requêtes concurrentes d’une seule connexion sont sérialisées sur un verrou. Pour prendre en charge plus de requêtes concurrentes, augmentez la taille du pool de connexions selon vos besoins plutôt que de désactiver thread_sensitive.
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.