La programmation concurrente en python

Python (source : wiki­me­dia com­mons, licence CC-BY-SA‑4.0 )

Ce tuto­riel est une tra­duc­tion infi­dèle d'un article de real​py​thon​.com https://​real​py​thon​.com/​p​y​t​h​o​n​-​c​o​n​c​u​r​r​e​n​c​y​/​#​w​h​e​n​-​t​o​-​u​s​e​-​c​o​n​c​u​r​r​e​ncy

Mer­ci à eux pour leur for­mi­dable tra­vail et leur auto­ri­sa­tion.

Vous avez cer­tai­ne­ment enten­du par­ler de la librai­rie asyn­cio qui a été ajou­té à Python 3 et vous êtes curieux de savoir com­ment elle se place par rap­port aux autres méthodes de pro­gram­ma­tions concur­rentes ? Vous vou­lez savoir ce qu'est la pro­gram­ma­tion concur­rente et com­ment cela pour­rait accé­lé­rer vos pro­grammes ? Vos don­nées sont trop grosses et vos cal­culs ou vos requêtes prennent des heures ? Vous êtes au bon endroit !
Dans ce tuto­riel nous allons voir :
- ce qu'est la pro­gram­ma­tion concur­rente
- ce qu'est la paral­lé­li­sa­tion
- les dif­fé­rence entre les méthodes de pro­gram­ma­tion concur­rente (threa­ding, asyn­cio et mul­ti­pro­ces­sing)
- com­ment uti­li­ser la pro­gram­ma­tion concur­rente dans vos pro­grammes.

Évi­de­ment la paral­lé­li­sa­tion ne rem­pla­ce­ra jamais une bonne opti­mi­sa­tion algo­rith­mique de votre code et un code bien opti­mi­sé ira tou­jours plus vite une fois paral­lé­li­sé qu'un code mal opti­mi­sé.

Ceci dit, entrons sans plus attendre dans le monde mer­veilleux de la pro­gram­ma­tion concur­rente.

Qu'est-ce que la concurrence (ou programmation concurrente) ?

La concur­rence c'est l'occurrence simul­ta­née de plu­sieurs évé­ne­ments. En Python, les choses qui se pro­duisent simul­ta­né­ment sont appe­lées par des noms dif­fé­rents (thread, tâche, pro­ces­sus), mais à un niveau éle­vé
elles font toutes réfé­rence à une séquence d'instructions qui s'exécutent dans l'ordre. On pour­rait les consi­dé­rer comme des cou­rants de pen­sées dif­fé­rents. Cha­cun d'entre eux peut être arrê­té à cer­tains endroits,
et le pro­ces­seur ou le cer­veau qui les traite peut pas­ser à un autre. L'état de cha­cun d'eux est sau­ve­gar­dé pour pou­voir être redé­mar­ré là où il a été inter­rom­pu.
Vous vous deman­dez peut-être pour­quoi Python uti­lise des mots dif­fé­rents pour le même concept. Il s'avère que les threads, les tasks et les pro­ces­sus ne sont iden­tiques que si vous les visua­li­sez à haut niveau. Une fois que vous com­men­cez à fouiller dans les détails, ils repré­sentent tous des choses légè­re­ment dif­fé­rentes. Vous ver­rez de plus en plus en quoi ils sont dif­fé­rents au fur et à mesure que vous pro­gres­sez dans les exemples.

Par­lons main­te­nant de la par­tie simul­ta­née de cette défi­ni­tion. Il faut être pru­dent parce que, dans les détails, seul le mul­ti­pro­ces­sing fait fonc­tion­ner ces pro­ces­sus en même temps. Le mul­ti­threa­ding et l'asyn­cio tournent tous deux sur un seul pro­ces­seur et ne fonc­tionnent donc pas réel­le­ment en paral­lèle. Ils trouvent sim­ple­ment des moyens intel­li­gents de se relayer pour accé­lé­rer le pro­ces­sus glo­bal. Même s'ils n'exécutent pas dif­fé­rents cou­rants de pen­sées simul­ta­né­ment, nous appe­lons tou­jours cela la concur­rence.

La façon dont les threads ou les tasks se suc­cèdent est la grande dif­fé­rence entre le mul­ti­threa­ding et l'asyn­cio. Dans le mul­ti­threa­ding, le sys­tème d'exploitation connaît réel­le­ment chaque thread et peut l'interrompre à tout moment pour lan­cer un thread dif­fé­rent. C'est ce qu'on appelle le "pre-emp­tive mul­ti­tas­king" car le sys­tème d'exploitation peut reprendre la main sur votre thread pour effec­tuer le chan­ge­ment.

Le "pre-emp­tive mul­ti­tas­king" est pra­tique dans la mesure où le code dans le thread n'a pas besoin de faire quoi que ce soit pour effec­tuer le chan­ge­ment. Cela peut aus­si être dif­fi­cile à cause de cette phrase "à tout moment". Ce chan­ge­ment peut se pro­duire au milieu d'une seule ins­truc­tion Python, même une ins­truc­tion tri­viale comme x = x + 1.

Asyn­cio, par contre, uti­lise le "coope­ra­tive mul­ti­tas­king". Les tâches doivent coopé­rer en annon­çant quand elles sont prêtes à être rem­pla­cées. Cela signi­fie que le code de la tâche doit être légè­re­ment modi­fié pour que cela se pro­duise.

L'avantage d'effectuer ce tra­vail sup­plé­men­taire dès le départ est que vous savez tou­jours où votre tâche sera échan­gée. Il ne sera pas échan­gé au milieu d'une ins­truc­tion Python à moins que cette ins­truc­tion ne soit mar­quée. Vous ver­rez plus loin com­ment cela peut sim­pli­fier cer­taines par­ties de votre concep­tion.

Qu'est-ce que le parallélisme ?

Jusqu'à pré­sent, vous avez exa­mi­né la concur­rence qui se pro­duit sur un seul pro­ces­seur. Qu'en est-il de tous ces cœurs de CPU que votre nou­vel ordi­na­teur por­table pos­sède ? Com­ment les uti­li­ser ? Le mul­ti­pro­ces­sing est la réponse.

Avec le mul­ti­pro­ces­sing, Python crée de nou­veaux pro­ces­sus. Un pro­ces­sus ici peut être consi­dé­ré comme un pro­gramme presque com­plè­te­ment dif­fé­rent, bien que tech­ni­que­ment il soit géné­ra­le­ment défi­ni comme une col­lec­tion de res­sources incluant de la mémoire, des ges­tion­naires de fichiers et d'autres choses. Une façon d'y pen­ser est que chaque pro­ces­sus s'exécute dans son propre inter­pré­teur Python.

Parce qu'il s'agit de pro­ces­sus dif­fé­rents, cha­cun de vos pro­ces­sus dans un pro­gramme mul­ti­pro­ces­seur peut fonc­tion­ner sur un noyau dif­fé­rent. Fonc­tion­ner sur un cœur dif­fé­rent signi­fie qu'ils peuvent réel­le­ment fonc­tion­ner en même temps, ce qui est fabu­leux. Il y a quelques com­pli­ca­tions qui découlent de pro­cé­der comme cela, mais Python les gère bien la plu­part du temps.

différences concurrences parallélisme :
Type de pro­gram­ma­tion concur­renteDéci­sion de chan­ge­mentNombre de pro­ces­seurs
pre-emp­tive mul­ti­tas­king
(mul­ti­threa­ding)
le sys­tème d'exploitation décide quand chan­ger de thread1
com­pre­hen­sive mul­ti­tas­king
(asyn­cio)
les tache décident quand rendre le contrôle1
mul­ti­pro­ces­singles pro­ces­sus tournent en paral­lèle sur dif­fé­rents pro­ces­seurs*beau­coup


Thread versus Processus

Voyons main­te­nant en pra­tique les points com­muns et dif­fé­rences entre Thread et Pro­ces­sus.

Un pro­ces­sus est une abs­trac­tion simple du sys­tème d'exploitation. C'est un pro­gramme qui est lui-même une exé­cu­tion — en d'autres termes, du code qui fait tour­ner un autre code. De nom­breux pro­ces­sus tournent tou­jours dans un ordi­na­teur, et s'exécutent en paral­lèle.

Un pro­ces­sus peut conte­nir plu­sieurs tâches. Ils exé­cutent le même code appar­te­nant au pro­ces­sus parent. Idéa­le­ment, ils tournent en paral­lèle, mais pas néces­sai­re­ment. Les pro­ces­sus sont insuf­fi­sants parce que les appli­ca­tions néces­sitent d'être atten­tif aux actions des uti­li­sa­teurs, tout en met­tant à jour l'affichage et en sau­ve­gar­dant un fichier.

différences pratiques entre thread et processus :
PROCESSUSTHREAD
Les pro­ces­sus ne par­tagent pas la mémoireLes tâches par­tagent la mémoire
Multiplier/​échanger de pro­ces­sus est oné­reuxMultiplier/​échanger des tâches est moins coû­teux
Les pro­ces­sus demande plus de res­sourcesLes tâches sont moins gour­mandes en res­source (on les appelle par­fois des pro­ces­sus légers)
Pas besoin de syn­chro­ni­ser la mémoireVous aurez besoin de méca­nismes de syn­chro­ni­sa­tion si vous sou­hai­tez mani­pu­ler cor­rec­te­ment la don­née

Les taches de la pro­gram­ma­tion asyn­chrone ne sont pas pré­sentes dans ce tableau. On peut dire que selon le contexte, leurs carac­té­ris­tiques et leur uti­li­té, sont pra­ti­que­ment les mêmes que si on avait uti­li­sé des threads. Le coût en res­sources reste moindre que les threads et sur­tout que c'est la tâche elle-même qui décide de rendre ou non le contrôle et non pas le sys­tème d'exploitation, ne néces­si­tant pas d'action de la part du sys­tème d'exploitation.

Chaque type de pro­gram­ma­tion concur­rente a son uti­li­té. Nous allons main­te­nant voir quel type de pro­gramme ils peuvent amé­lio­rer.

Quand la concurrence est-elle utile ?

La pro­gram­ma­tion concur­rente peut faire une grande dif­fé­rence pour deux types de pro­blèmes. Ces pro­blèmes sont géné­ra­le­ment appe­lés "limi­tés par le CPU" et "limi­tés par les entrées/​sorties (E/​S)".

Les pro­blèmes limi­tés aux entrée/​sortie (E/​S) ralen­tissent votre pro­gramme parce qu'il doit sou­vent attendre l'E/S d'une res­source externe. Ils sur­viennent fré­quem­ment lorsque votre pro­gramme tra­vaille avec des choses qui sont beau­coup plus lentes que votre CPU .

Les exemples de choses qui sont plus lentes que votre CPU sont légion, mais heu­reu­se­ment votre pro­gramme n'interagit pas avec la plu­part d'entre eux. Les choses lentes avec les­quelles votre pro­gramme inter­agi­ra le plus sou­vent sont le sys­tème de fichiers et les connexions réseau (disque dure, NAS, base de don­nées , requête http).

Voyons à quoi cela res­semble :

Diagramme de temps d'un programme limité par les E/​S :
diagramme d'execution d'un programme limité par les entrées/sorties

Dans le dia­gramme ci-des­sus, les cases bleues indiquent le temps pen­dant lequel votre pro­gramme tra­vaille, et les cases rouges indiquent le temps pas­sé à attendre qu'une opé­ra­tion d'E/S soit ter­mi­née. Ce dia­gramme n'est pas à l'échelle car les requêtes sur Inter­net peuvent prendre plu­sieurs ordres de gran­deur plus longs que les ins­truc­tions CPU, de sorte que votre pro­gramme puisse pas­ser la plu­part de son temps à attendre. C'est ce que fait votre navi­ga­teur web la plu­part du temps.

D'un autre côté, il y a des classes de pro­grammes qui font des cal­culs signi­fi­ca­tifs sans par­ler au réseau ou accé­der à un fichier. Ce sont les pro­grammes limi­tés par le CPU parce que la res­source limi­tant la vitesse de votre pro­gramme est le CPU et non pas le réseau ou le sys­tème de fichiers.

Voici un diagramme correspondant pour un programme lié au CPU :
diagramme d'execution d'un programme limité par le CPU

Dans les pro­chains articles, vous ver­rez que les dif­fé­rentes formes de concur­rences fonc­tionnent mieux ou moins bien avec les pro­grammes liés au CPU et les pro­grammes liés aux E/​S. L'ajout de la concur­rence à votre pro­gramme ajoute des options et des com­pli­ca­tions, vous devrez donc déci­der si l'accélération poten­tielle vaut la peine d'un effort sup­plé­men­taire. D'ici la fin de ce tuto­riel, vous devriez avoir suf­fi­sam­ment d'informations pour com­men­cer à prendre cette déci­sion.

problème limité par les entrées/​sorties vs problème limité par le CPU :
Pro­blème limi­té par les entrées/​sortie (IO bound)Pro­blème limi­té par le CPU (CPU bound)
Le pro­gramme com­mu­nique avec un péri­phé­rique lent comme une connexion réseau, un disque dur ou une impri­manteLe pro­gramme fait majo­ri­tai­re­ment de opé­ra­tion de cal­cul CPU
L’accélérer implique majo­ri­tai­re­ment de super­po­ser les temps d'attente des péri­phé­riques
L’accélérer implique de faire plus de cal­culs dans une même quan­ti­té de temps

Dans le pro­chain article, nous exa­mi­ne­rons les pro­grammes limi­tés par les E/​S. Ensuite, nous ver­rons les pro­grammes limi­tés par CPU. Et pour finir quelque piste sur com­ment uti­li­ser la concur­rence pour accé­lé­rer votre pro­gramme python.

Mer­ci a Yoann M, WJ et Kevin Gueu­ti pour la relec­ture atten­tive.


  1. *
    dif­fé­rents types de concur­rence


Pour continuer la lecture :


Commentaires

2 réponses à “La programmation concurrente en python”

  1. Bon­jour,

    un sujet tou­jours inté­res­sant. Dans ce cadre, l'article de sam et max est aus­si très bien :
    http://​samet​max​.com/​l​a​-​d​i​f​f​e​r​e​n​c​e​-​e​n​t​r​e​-​l​a​-​p​r​o​g​r​a​m​m​a​t​i​o​n​-​a​s​y​n​c​h​r​o​n​e​-​p​a​r​a​l​l​e​l​e​-​e​t​-​c​o​n​c​u​r​r​e​n​te/

    dom­mage que l'article fasse com­plè­te­ment abs­trac­tion du GIL, qui est une rai­son pour laquelle le mul­ti­pro­ces­sing est impor­tant et par­fois com­pli­qué en python.

    Par ailleurs plu­sieurs infor­ma­tions sont dis­cu­tables dans l'article. Par exemple il est pos­sible de par­ta­ger la mémoire entre les pro­ces­sus, c'est juste com­pli­qué. Dans ce cadre, la der­nière ver­sion de python (3.8) intro­duit de nou­velles fonc­tions utiles dans le par­tage de mémoire entre les pro­ces­sus :
    https://​docs​.python​.org/​d​e​v​/​l​i​b​r​a​r​y​/​m​u​l​t​i​p​r​o​c​e​s​s​i​n​g​.​s​h​a​r​e​d​_​m​e​m​o​r​y​.​h​tml

    De plus je trouve ça étrange de dire que les pro­ces­sus "consomment plus de res­source", étant don­né que c'est inhé­rent à la mise en place d'un nou­veau pro­ces­sus. Mais en com­pa­rai­son aux besoins requit lors de la mise en place de mul­ti­pro­ces­sing, c'est assez négli­geable en fait.

    JS

    1. Avatar de Aurélien Béliard
      Aurélien Béliard

      mer­ci pour ce com­men­taire très per­ti­nent. En effet le GIL rend le mul­ti­threa­ding com­pli­qué en python, j'en parle un peu dans la par­tie 2 ou je détaille un pb iobound ( j'aurais peut être du en par­lé aus­si dans cette par­tie) j'ai choi­si dans cette par­tie de mettre l'accent sur les points com­muns et dif­fé­rences théo­riques entre les dif­fé­rentes méthodes. Je dois avoué que je ne connais­sais pas la méthode sha­red memo­ry en effet je ne suis pas pas­sé en python3.8 . Mon focus sur le par­tage de mémoire entre thread avait aus­si pour but de par­ler un peu de race condi­tion dans la par­tie sui­vante

Laisser un commentaire