En Python, le code s'exécute de manière synchrone : c'est-à-dire que chaque ligne de code doit attendre que la précédente soit terminée pour s'exécuter. Si une tâche prend du temps, alors il faudra attendre qu'elle se termine pour passer à la suite du programme.
Heureusement, il est possible d'utiliser les mots-clés async et await. Au lieu de bloquer l'exécution, Python peut mettre en pause une tâche pour avancer sur une autre.
Qu'est-ce que le GIL ?
Pour une explication complète sur le GIL, n'hésitez pas à aller voir notre glossaire sur le sujet.
Le GIL (Global Interpreter Lock) contraint Python à n'exécuter qu'une instruction à la fois dans le thread. Prenons un exemple :
import time def tache_longue(nom): print(f"Début tâche {nom}") # Simulons une tâche longue avec sleep time.sleep(1) print(f"Fin tâche {nom}") debut = time.time() tache_longue("A") # Attend 1 seconde tache_longue("B") # Attend encore 1 seconde print(f"Temps total : {time.time() - debut}s") # Résultat : Temps total : Environ 2 secondes
Il faut attendre 2 secondes pour que le programme se termine. Ce qui est inefficace pour des opérations d'E/S (I/O), comme les attentes lors d'appels réseau.
asyncio, async et await : la solution
La programmation asynchrone permet d'optimiser l'exécution d'un thread unique. Python pourra lancer une tâche et, pendant qu'elle attend, en lancer une autre.
Pour définir une fonction asynchrone (on parlera de coroutine), on utilise async def. Et pour utiliser un résultat sans bloquer, on utilise le mot-clé await.
Reprenons l'exemple précédent :
import time import asyncio async def tache_longue(nom): print(f"Début tâche {nom}") # Utilisation de asyncio.sleep pour ne pas bloquer l'événement await asyncio.sleep(1) print(f"Fin tâche {nom}") async def main(): debut = time.time() await asyncio.gather( tache_longue("A"), # Attend 1 seconde tache_longue("B") # Attend encore 1 seconde ) print(f"Temps total : {time.time() - debut}s") asyncio.run(main()) # Temps total : Environ 1 seconde
Pas mal de changements ici, décortiquons le code :
-
async defpermet de transformer la fonction en coroutine -
await asyncio.sleep(1)sert à simuler une opération comme l'attente de la réponse d'un serveur -
asyncio.gatherpermet de lancer plusieurs tâches en même temps -
asyncio.run(main())démarre l'exécution asynchrone
Le temps d'exécution est divisé par deux. Pendant qu'une tâche est en pause, Python en profite pour avancer sur une autre.
Quand utiliser l'asynchrone avec Python ?
Attention, l'asynchrone n'est pas toujours la bonne approche. Pour les opérations I/O Bound comme les requêtes HTTP, l'accès à la base de données ou la lecture de fichiers, asyncio est tout à fait adapté.
Cependant, vous pouvez vous retrouver dans le cas où les lenteurs potentielles sont liées au processeur ; on parlera de CPU Bound. C'est le cas, par exemple, si vous effectuez des calculs lourds, du traitement d'images, etc.
Pourquoi ne pas prendre le temps d'aller lire notre glossaire sur multiprocessing ? 📚
Quelle est la différence entre async et threading en Python ?
Bien que les modules asyncio et threading servent tous les deux à faire de la concurrence, leur fonctionnement est différent :
-
asyncioutilise un seul thread et bascule entre les tâches. C'est très efficace au niveau de la gestion de la mémoire -
threadingcrée plusieurs threads, ce qui est plus gourmand en mémoire
Pourquoi utiliser threading alors ?
De nombreuses bibliothèques Python ne sont pas compatibles avec asyncio (comme requests, par exemple). Dans ce cas précis, on préférera utiliser le multithreading avec threading. Nous avons un glossaire dédié au threading sur Docstring.
On pourrait aussi évoquer run_in_executor de asyncio, qui permet de lancer du code synchrone tout en utilisant des bibliothèques non asynchrones.