Initiation à la programmation concurrente
Notion de performance
Avant de parler de performance, il faut d’abord savoir analyser quels sont les paradigmes utilisés, quelle est la complexité du programme et enfin, quelle est l’architecture matérielle qui va faire tourner le programme.
En effet, il y a énormément de détails sur lesquels le programme n’a aucune maîtrise. On peut citer, par exemple, la nature du système d’exploitation qui le fait tourner, la quantité et la consommation des autres programmes qui tournent à coté, la quantité de mémoire, le processeur ou encore le débit réseau.
Sachez qu’on ne parle pas, dans cette partie, de la performance dans le sens optimisation au niveau algorithmique du code, mais de solutions au niveau de l’architecture de l’application elle-même.
1. Programmation bloquante
Tout programme passe par des phases où il est en pause. Ceci peut être dû au fait qu’il attende quelque chose de l’utilisateur :
data = read("Saisissez une donnée")
Ou parce qu’il attend après une ressource quelconque : I/O, disque dur, périphérique, ou encore réseau :
import requests
response = requests.get('http://inspyration.org')
Dans cet exemple, le programme est en attente de la résolution de nom (DNS), puis de la réponse du serveur qui va potentiellement mettre la requête en file d’attente puis travailler de son côté pendant un petit moment avant de commencer à envoyer sa réponse.
Dans un cas comme dans l’autre, pendant que votre programme attend la ressource, il est bloqué : il ne fait rien d’autre. Suivant le contexte, ceci peut être acceptable, car on peut se permettre de perdre jusqu’à 300, voire 500 millisecondes sans que l’utilisateur n’en ressente la gêne.
Cependant, dans d’autres contextes, ceci est plus ennuyeux :
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.content, "html.parser")
image_urls = [img.get("src") for img in soup.find_all("img")]
image_contents = [requests.get("http://inspyration.org/{}".format(url)) ...
Terminologie
On va présenter dans cette partie quelques notions techniques qu’il est nécessaire de comprendre pour faire ses choix.
1. Processus
Un processus est l’exécution d’un ensemble d’instructions par l’utilisation de ressources physiques, les deux principales étant la mémoire vive dans laquelle il stocke son environnement d’exécution et le processeur qui lui est affecté.
Les opérations de création, gestion et destruction d’un processus sont gérées par le système d’exploitation, pas directement par Python. Python propose des outils de haut niveau pour créer, gérer et détruire ces processus qui restent relativement simples, mais il utilise en réalité le système d’exploitation qui fait l’essentiel du travail.
Et heureusement, car ce dernier est extrêmement complexe et fait intervenir des notions de programmation système très pointues. Le processus est géré par un ordonnanceur qui est dépendant du système d’exploitation. Ce dernier est chargé de mettre à disposition les ressources (mémoire, temps processeurs...) et de veiller à ce que chaque processus accède à ces ressources de manière équitable. S’il y a plusieurs processeurs, les processus sont également distribués entre eux de manière équitable.
Un programme lancé par un utilisateur peut correspondre à un seul processus ou à plusieurs. Par exemple, au lancement, Apache crée six processus :
$ ps ax | grep apache
1569 ? Ss 0:00 /usr/sbin/apache2 -k start
1584 ? S 0:00 /usr/sbin/apache2 -k start
1585 ? S 0:00 /usr/sbin/apache2 -k start
1586 ? S 0:00 /usr/sbin/apache2 -k start ...
Présentation des paradigmes
1. Programmation asynchrone
Lorsque la problématique principale est le temps I/O, la solution idéale est de programmer en asynchrone. L’idée est que l’on continue à faire tourner notre programme par un processus ne contenant qu’un fil d’exécution, on ne s’embête pas à gérer des tâches ou des processus, qui sont un outil relativement lourd à mettre en place.
La programmation asynchrone consiste à déclarer qu’une des tâches que le programme doit exécuter peut être potentiellement bloquante (être en attente d’une I/O et n’avoir rien à faire pendant ce temps) et le programme pourra alors continuer à exécuter la suite du code en attendant.
C’est, par exemple, un des points forts de JavaScript, un langage qui est conçu avec des objectifs particuliers : il est exécuté par un navigateur et c’est le navigateur qui gère les fils d’exécution et processus. JavaScript aura donc toujours, dans ce contexte, un seul fil d’exécution et un seul processus et étant donné qu’il exécute des instructions dans un contexte réseau, il y a forcément énormément d’attente I/O.
C’est donc pour cela que le levier asynchrone est pour ce langage la meilleure option pour améliorer la performance et c’est pour cette raison qu’il y excelle, tous les efforts étant portés vers cela.
Ainsi, lorsque l’on vante les mérites de Node.js, on ne fait que vanter la bonne utilisation des capacités du langage lui-même. Node.js fait simplement une bonne utilisation de l’asynchrone et sait en tirer parti. C’est aussi ce que d’autres serveurs Python très anciens et très connus font parfaitement depuis des années, tels que Tornado ou Twisted, mais chacun à sa manière.
Un des axes d’amélioration de la branche 3.x de Python est justement le développement de ces capacités de gestion de l’asynchrone. Le chapitre Programmation asynchrone : initiation est dédié à la présentation de ces fonctionnalités et présente quelques exemples concrets.