===== Python : programmation asynchrone =====
===== Présentation =====
Un **programme** est une succession d'instructions qui s'**exécutent plus ou moins rapidement** :
* si ces instructions doivent **obligatoirement s'enchaîner** parce qu'elles sont **dépendantes** les unes des autres, le **temps d'exécution de la totalité du programme** résultera de la **somme du temps d'exécution individuel** de chaque instruction.
* si certaines instructions sont **indépendantes** entre elles, elles seront cependant **bloquées** le temps que se **terminent** les instructions précédentes.
Ce mode de programmation est un appelé **programmation synchrone**.
Le mode **programmation asynchrone** permet alors de **lancer** l'exécution d'une instruction et, **sans attendre** la fin de l'exécution de celle-ci, de **lancer** l'exécution d'une autre instruction.
Cela est utile pour des instructions qui s'**exécutent lentement** comme la récupération ou l'écriture de données depuis le **réseau** ou sur le disque dur (les entrée/sorties) et que l'on souhaite na pas **bloquer** le programme tant que ces instructions ne sont pas **terminées**. Cela concerne donc particulièrement les **applications du Web**.
La programmation **asynchrone** permet de gérer un ensemble **d’événements** de façon **concurrente**.
===== Principe de la programmation asynchrone =====
L'idée générale de la programmation asynchrone est de pouvoir **démarrer des opérations non bloquantes** de façon **concurrente**, puis de pouvoir suivre leurs états et finalement exécuter des fonctions de retour (callbacks) quand elles sont prêtes.
Pour cela le programme va **gérer une boucle** qui attend que des **événements** (event loop) se produisent et alors fait **correspondre** à chaque événement qui survient la **fonction** qui lui est associée. C'est dans la fonction associée que sont définis les traitement à faire.
**Exemple pour un serveur Web** :
* le serveur Web à un **seul point d'entrée** et dispose de **plusieurs pages web** ;
* le serveur est en **attente**, c'est à dire à **l'écoute** des requêtes des clients (les navigateurs) qui peuvent demander telle ou telle page ;
* dès qu'une **requête arrive**, le serveur Web **retourne la page correspondant à la demande**.
Précisions :
* **chaque** requête des clients est considérée comme un **événement distinct** (event),
* le serveur est programmé pour qu'à **chaque** événement une **fonction prédéfinie** soit exécutée. L'événement est le **déclencheur** de l'exécution de la fonction.
===== La bibliothèque asyncio =====
La bibliothèque **asyncio** permet de faire de la programmation asynchrone et dispose de deux éléments fondamentaux, une **event loop** et la **classe Future**.
La **mise en oeuvre** de la programmation asynchrone avec la bibliothèque asyncio nécessite l'utilisation deux mots clés **async** et **await** :
* async permet de **définir la fonction comme asynchrone**,
* dans le corps de la fonction, **await** permet de préciser qu'une instruction **suspend l’exécution** de la fonction asynchrone le temps que cette instruction se **termine**. Pendant ce temps le programme qui a appelé la fonction asynchrone pourra **continuer son exécution** normalement en attendant qu’elle soit **débloquée**.
===== Première manière de gérer une boucle d'événement =====
Première possibilité pour **créer** une boucle d'événement (event loop) :
* on **instancie** une **boucle d'événement** avec la méthode **asyncio.get_event_loop()**
* on précise, durant cette bouche d'événement **quelle fonction** doit être **exécutée** avec la méthode **run_until_complete(maCoroutine())**. La fonction qui est exécutée dans la boucle d'événement est appelée une **coroutine**.
* on définit une fonction comme un coroutine en rajoutant le mot clé **async**.
import asyncio
import time
# Definir une coroutine qui sera exécutée ultérieurement (dans le futur)
async def maCoroutine():
print("Début de ma Coroutine")
time.sleep(2)
print("Fin de ma Coroutine")
# Faire tourner une simple boucle d'événement
# jusqu'à ce que l'exécution a la coroutine soit finie (completed)
loop = asyncio.get_event_loop()
loop.run_until_complete(maCoroutine())
# fermer la boucle d'événement
print("fermer la boucle d'événement")
loop.close()
===== Les options d'exécution =====
Pour exécuter la boucle d'événement on peut :
* appeler la méthode **run_until_complete(future)** et terminer la boucle d'événement quand la fonction future (la coroutine) a **terminé** son exécution,
* appeler la méthode **run_forever()** qui exécutera la boucle d'événement jusqu'à ce que la méthode **stop() soit appelée** ou si le programme se termine.
===== Autre alternative pour gérer une boucle d'événement avec la méthode run_forever() =====
La méthode **run_forever()** permet à la boucle d'événement de s'exécuter **indéfiniment** jusqu'à ce que le programme se termine ou si la méthode **stop()** est appelée.
La méthode **ensure_future()** permet également de déclarer une coroutine et sera utile pour déclarer plusieurs coroutines dans la même boucle d'événement.
import asyncio
async def maCoroutine():
while True:
print("Début de ma Coroutine")
await asyncio.sleep(1)
print("Fin de ma Coroutine")
loop = asyncio.get_event_loop()
try:
asyncio.ensure_future(maCoroutine())
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
# fermer la boucle d'événement
print("fermer la boucle d'événement")
loop.close()
**Précisions** : \\
La fonction utilise une **boucle infinie** et donc le programme va **boucler indéfiniment**. Il est possible alors
:
* **d'interrompre** le programme avec les touches d'interruption **CTRL + C**,
* de **gérer l'exceptio**n qui est générée,
* pour afficher **Fermer proprement** la boucle et **terminer proprement** le programme.
===== Gérer des tâches en parallèle =====
Un intérête de la programmation asynchrone est de pourvoir gérer des tâches en parallèle :
* une première tâche est **lancée**,
* une **deuxième** tâche, qui ne dépend par de la fin de l'exécution de la première tâche peut être **lancée**, alors même que la première tâche n'est **pas terminée**.
import asyncio
async def tache_1():
while True:
print("Début de ma tâche 1")
await asyncio.sleep(1)
print("Fin de ma tâche 1")
async def tache_2():
while True:
print("Début de ma tâche 2")
await asyncio.sleep(5)
print("Fin de ma tâche 2")
loop = asyncio.get_event_loop()
try:
asyncio.ensure_future(tache_1())
asyncio.ensure_future(tache_2())
loop.run_forever()
except KeyboardInterrupt:
pass
finally:
# fermer la boucle d'événement
print("Fermer la boucle d'événement")
loop.close()
Voici le résultat obtenu :
>>>
RESTART: D:/Developpement/python/Websocket/scripts/coroutine_future_parallele.py
Début de ma tâche 1
Début de ma tâche 2
Fin de ma tâche 1
Début de ma tâche 1
Fin de ma tâche 1
Début de ma tâche 1
Fin de ma tâche 1
Début de ma tâche 1
Fin de ma tâche 1
Début de ma tâche 1
Fin de ma tâche 2
Début de ma tâche 2
Fin de ma tâche 1
Début de ma tâche 1
Fin de ma tâche 1
Début de ma tâche 1
Fermer la boucle d'événement
>>>
La tâche 1 **s'exécute plus souvent** que la tâche 2. Ces deux tâches s'exécutent **indépendamment** l'une de l'autre.
==== Retour au cours : Les instructions du langage Python ====
* [[.:c_langage_python|Cours : Les instructions du langage Python]]