Les problèmes limités par les entrées/​sorties (IObound)

Dans la pre­mière par­tie de ce tuto­riel , j'ai expli­qué ce qu’était la pro­gram­ma­tion concur­rente et paral­lèle, ain­si que détaillé les dif­fé­rents types de pro­gram­ma­tion concur­rente et leurs spé­ci­fi­ci­tés. Si vous ne l'avez pas lue, je vous conseille de la lire avant de démar­rer. Dans cette deuxième par­tie, nous allons nous concen­trer sur l'optimisation d'un pro­gramme limi­té par les entrées/​sorties grâce à la pro­gram­ma­tion concur­rente.

Qu'est ce qu'un pro­gramme limi­tés par les entrées/​sorties ?

C'est un pro­gramme qui com­mu­nique régu­liè­re­ment avec une res­source plus lente que le CPU (accès au disque dur, bande pas­sante d'un réseau, etc.). Lors de ces com­mu­ni­ca­tions le CPU va attendre que la res­source en ques­tion lui envoie des infor­ma­tions. C'est cette attente qui est un des fac­teurs d'augmentation du temps d’exécution d'un pro­gramme. Or plus le nombre de com­mu­ni­ca­tions, d'entrées/sorties du CPU est impor­tant plus le pro­gramme sera lent. On retrouve ce cas de figure lors de requêtes en base de don­nées, lors de requêtes HTTP ou pour les connec­tions réseaux en géné­ral.

Comment accélérer un programme limité par les E/​S :

Pour illus­trer mon pro­pos, j'ai choi­si un pro­blème com­mun : le télé­char­ge­ment de conte­nu sur le réseau. J'ai eu envie d'aller récu­pé­rer des infor­ma­tions sur des per­son­nage de Star Wars en uti­li­sant la magni­fique API https://​swa​pi​.co/ .

La version synchrone du programme

Le dia­gramme d'exécution res­semble à ça :

Quelles sont les avantages de la version synchrone ?

La ver­sion syn­chrone est super facile à écrire, à conce­voir et à débog­guer. Dans cette ver­sion le pro­gramme ne fonc­tionne que sur un seul CPU, de sorte qu'il est très facile de pré­dire la pro­chaine étape et com­ment elle va se com­por­ter.

En effet, chaque opé­ra­tion ne sera exé­cu­tée que si la pré­cé­dente s'est ter­mi­née, ain­si pour récu­pé­rer des infor­ma­tions sur le Géné­ral Grie­vous (qui est le per­son­nage N°79) nous savons que nous devons d'abord récu­pé­rer les infor­ma­tions des 78 per­son­nages pré­cé­dents.

Les problèmes avec la version synchrone

Le pro­blème ici, c'est que c'est que lorsque nous deman­dons au ser­veur de nous ren­voyer des infor­ma­tions sur un per­son­nage nous devons attendre sa réponse avant de pou­voir deman­der qu'il nous envoie le sui­vant. Cette méthode est rela­ti­ve­ment lente par rap­port aux autres solu­tions que nous allons pro­po­ser. Voi­ci le temps d’exécution sur ma machine :

$ python io_bound_problem_non_concurrent.py
Down­loa­ded 79 in 14.642319917678833 seconds

Remarque : Vos résul­tats peuvent varier consi­dé­ra­ble­ment selon votre connexion. En exé­cu­tant ce script, j'ai noté des temps variant de 14.2 à 21.9 secondes. Pour cet article, j'ai pris le temps le plus rapide des trois exé­cu­tion.

Il faut bien réflé­chir avant d'ajouter de la pro­gram­ma­tion concur­rente, en effet si le pro­gramme n'est pas exé­cu­té sou­vent, ce n'est pas grave qu'il soit lent mais que se passe-t-il si il est uti­li­sé fré­quem­ment et qu'il prend des heures pour chaque exé­cu­tion ? Il est peut être inté­res­sant d'y ajou­ter de la concur­rence, par exemple en le réécri­vant à l'aide du mul­ti-threa­ding.

Version multi-threadée

Comme vous l'avez pro­ba­ble­ment devi­né, écrire un pro­gramme mul­ti-threa­dé demande légè­re­ment plus de tra­vail. Vous serez peut-être sur­pris du peu d'efforts sup­plé­men­taires qu'il faut pour les cas simples. Voi­ci à quoi res­semble le même pro­gramme avec le mul­ti-threa­ding :

Lorsque vous ajou­tez des threads, la struc­ture géné­rale est la même et vous n'avez besoin que de quelques chan­ge­ments. download_​all_​sites() a chan­gé de l'appel de la fonc­tion une fois par site en une struc­ture plus com­plexe.

Dans cette ver­sion, vous créez un Thread­Poo­lExe­cu­tor, ce qui semble com­pli­qué.
Nous allons le décom­po­ser : Thread­Poo­lExe­cu­tor = Thread + Pool + Exe­cu­tor.
Cet objet va créer un Pool (un ensemble) de Threadsqui peuvent fonc­tion­ner simul­ta­né­ment. Enfin, l'Exe­cu­tor est la par­tie qui va contrô­ler quand et com­ment cha­cun des threads du pool va s'exécuter.

La biblio­thèque stan­dard implé­mente Thread­Poo­lExe­cu­tor en tant que ges­tion­naire de contexte afin que vous puis­siez uti­li­ser la syn­taxe with pour gérer la créa­tion et la libé­ra­tion du pool de threads.

Une fois que vous avez un Thread­Poo­lExe­cu­tor, vous pou­vez uti­li­ser sa méthode .map(). Cette méthode exé­cute la fonc­tion trans­mise (ici download_​site) sur cha­cun des sites de la liste. L'avantage, c'est qu'il les exé­cute auto­ma­ti­que­ment en même temps en uti­li­sant le pool de threads qu'il gère.

Ceux venant d'autres lan­gages, ou même de Python 2, se demandent pro­ba­ble­ment où se trouvent les objets et fonc­tions habi­tuels qui gèrent les détails aux­quels vous êtes habi­tués lorsqu'il s'agit de threads, de choses comme Thread.start(), Thread.join() et Queue.

Ils sont tous tou­jours là, et vous pou­vez les uti­li­ser pour obte­nir un contrôle fin sur la manière dont vos threads sont exé­cu­tés. Mais, à par­tir de Python 3.2, la biblio­thèque stan­dard a ajou­té un niveau d'abstraction supé­rieur, les Exe­cu­tors, qui gère de nom­breux détails pour vous, donc vous n'avez plus besoin de ce contrôle fin.

L'autre chan­ge­ment inté­res­sant dans notre exemple est que chaque thread doit créer son propre objet requests.Session().
C'est l'une des ques­tions inté­res­santes et dif­fi­ciles avec le mul­ti-threa­ding. En effet, ce n'est pas Python mais le sys­tème d'exploitation qui contrôle le moment où votre thread est inter­rom­pu et qu'un autre thread démarre, ain­si toutes les don­nées qui sont par­ta­gées entre les threads doivent être pro­té­gées ou sécu­ri­sées. Mal­heu­reu­se­ment, requests.Session() n'est pas thread-safe.

Il existe plu­sieurs stra­té­gies pour sécu­ri­ser l'accès aux don­nées en fonc­tion de la nature des don­nées et de la façon dont vous les uti­li­sez. Une des stra­té­gies est d'utiliser des struc­tures de don­nées sécu­ri­sées comme Queue du module queue de Python.

Ces objets uti­lisent des pri­mi­tives de bas niveau comme threading.lock pour s'assurer qu'un seul thread peut accé­der à un bloc de code ou un bit de mémoire en même temps. Vous uti­li­sez cette stra­té­gie indi­rec­te­ment via l'objet Thread­Poo­lExe­cu­tor.

Une autre stra­té­gie à uti­li­ser ici est ce qu'on appelle le sto­ckage local de thread.
Threading.local() crée un objet qui res­semble à un glo­bal mais qui est spé­ci­fique à chaque thread indi­vi­duel. Dans votre exemple, cela se fait avec thread­Lo­cal et get_​session() :

Thread­Lo­cal est dans le module de threa­ding pour résoudre spé­ci­fi­que­ment ce pro­blème. Cela semble un peu étrange, mais vous ne vou­lez créer qu'un seul de ces objets, pas un pour chaque thread. L'objet lui-même se charge de sépa­rer les accès des dif­fé­rents threads aux dif­fé­rentes don­nées.

Quand get_​session() est appe­lé, la ses­sion qu'il consulte est spé­ci­fique au thread par­ti­cu­lier sur lequel il s'exécute. Ain­si, chaque thread crée une seule ses­sion la pre­mière fois qu'il appelle get_​session() et uti­li­se­ra sim­ple­ment cette ses­sion sur chaque appel sui­vant tout au long de sa vie.

Enfin, un petit mot sur le choix du nombre de threads. Vous pou­vez voir que l'exemple de code uti­lise 35 threads. N'hésitez pas à jouer avec ce nombre et à voir com­ment le temps total change. On pour­rait s'attendre à ce qu'avoir un thread par télé­char­ge­ment soit le plus rapide mais, du moins sur mon sys­tème, ce n'était pas le cas. J'ai trou­vé les résul­tats les plus rapides entre 30 et 40 threads. Si vous dépas­ser les 40 , alors les coûts sup­plé­men­taires de créa­tion et de des­truc­tion des threads sup­priment tout gain de temps.

Pourquoi la version multi-threadée est super efficace ?

Elle est très rapide. Rap­pe­lez vous que la ver­sion syn­chrone a pris plus de 14 secondes :

le dia­gramme d’exécution res­semble a :

Il uti­lise plu­sieurs threads pour avoir plu­sieurs requêtes vers des sites web ouvertes en même temps, ce qui per­met à votre pro­gramme de che­vau­cher les temps d'attente et d'obtenir le résul­tat final plus rapi­de­ment ! Cool ! C'était l'objectif.

Les problèmes avec la version multi-threadée

Comme vous pou­vez le voir dans l'exemple de la ver­sion mul­ti-threa­dée, il est très impor­tant de réflé­chir aux don­nées qui sont par­ta­gées entre les threads.

Les threads peuvent inter­agir de manière sub­tile. Ces inter­ac­tions peuvent pro­vo­quer des races condi­tions qui se tra­duisent sou­vent par des bogues aléa­toires et inter­mit­tents et qui peuvent être très dif­fi­ciles à trou­ver. Ceux d'entre vous qui ne sont pas fami­liers avec le concept des races condi­tions vou­dront peut-être déve­lop­per et lire la sec­tion ci-des­sous.

Version asynchrone

Avant d’étudier le code de l'exemple asyn­cio, nous allons par­ler plus en détail de la façon dont asyn­cio fonc­tionne à par­tir d'une ver­sion sim­pli­fiée de l'asyncio.

Asyncio : notions de base

Asyn­cio per­met de défi­nir un objet Python appe­lé boucle d’événement qui va contrô­ler quand et com­ment un ensemble de tâches sera exé­cu­tée.
Cet objet, cette boucle d'évènement, est consciente des dif­fé­rentes tâches à exé­cu­ter et son tra­vail est d'orchestrer ces der­nières de la meilleure façon pos­sible.
Pour ce faire elle connaît plu­sieurs choses. Pre­miè­re­ment, la liste des tâches à effec­tuer mais aus­si l'état dans lequel se trouve cha­cune de ces tâches.
Pre­nons un exemple simple de tâche à deux états (en réa­li­té il peut y en avoir beau­coup plus):

  • l'état PRÊT indi­que­ra qu'une tâche a du tra­vail à faire et est prête à être exé­cu­tée.
  • l'état ATTENTE signi­fie­ra que la tâche attend la fin d'un évé­ne­ment externe, comme une opé­ra­tion réseau.

Lorsqu'une tâche passe en attente (attente d'une réponse suite à une requête réseau par exemple) et ne peux plus pro­gres­ser elle va rendre le contrôle à la boucle d'évènement en lui com­mu­ni­quant exac­te­ment ce qu'elle attend. Notre boucle d'évènement va alors s'empresser de ran­ger cette tâche soit par­mi celles en ATTENTE soit par­mi celles PRÊTes. Elle va ensuite pas­ser en revue l'ensemble des tâches pour en exé­cu­ter une dont le sta­tus est pas­sé à PRÊT. Votre boucle d'événements sim­pli­fiée choi­sit la tâche qui a atten­du le plus long­temps et l'exécute. Ce pro­ces­sus se répète jusqu'à ce que la boucle d'événements soit ter­mi­née.

Un point impor­tant de l'asyncio est que ce sont les tâches qui redonnent le contrôle à la boucle d'évènement, jamais l'inverse. Elles ne sont donc jamais inter­rom­pues au milieu d'une opé­ra­tion mais seule­ment quand elles le jugent néces­saire. Cela nous per­met de par­ta­ger les res­sources un peu plus faci­le­ment en asyn­cio qu'en threa­ding. Vous n'avez pas à vous sou­cier de rendre votre code thread-safe.C'est une vue d'ensemble de ce qui se passe avec Asyn­cio. Si vous vou­lez plus d'informations, cette réponse Sta­ckO­ver­flow four­nie quelques bons détails.

async et await

Par­lons main­te­nant de deux nou­veaux mots-clés qui ont été ajou­tés à Python : async et await. À la lumière de la dis­cus­sion ci-des­sus, vous pou­vez voir await comme de la magie qui per­met à une tâche de rendre le contrôle à la boucle d'événements. Lorsque votre code attend un appel de fonc­tion, c'est un signal que l'appel est sus­cep­tible d'être quelque chose qui prend un cer­tain temps et que la tâche devrait aban­don­ner le contrôle.

Il est plus facile de pen­ser à async comme un flag pour Python lui disant que la fonc­tion sur le point d'être défi­nie uti­lise await. Il y a des cas où ce n'est pas stric­te­ment vrai, comme les géné­ra­teurs asyn­chrones, mais cela reste valable dans de nom­breuses situa­tions et vous donne un modèle simple pour débu­ter.

Une excep­tion à cela, que vous ver­rez dans le code sui­vant, est l'asynchrone avec l'instruction with qui crée un ges­tion­naire de contexte à par­tir d'un objet que vous auriez nor­ma­le­ment atten­du. Bien que la séman­tique soit un peu dif­fé­rente, l'idée est la même : signa­ler ce ges­tion­naire de contexte comme quelque chose qui peut être rem­pla­cé.

Comme je suis sûr que vous pou­vez l'imaginer, il y a une cer­taine com­plexi­té dans la ges­tion de l'interaction entre la boucle d'événements et les tâches. Pour les déve­lop­peurs débu­tant avec asyn­cio, ces détails ne sont pas impor­tants, mais vous devez vous rap­pe­ler que toute fonc­tion qui appelle await doit être mar­quée avec async. Sinon, vous obtien­drez une erreur de syn­taxe.

Retour au Code

Main­te­nant que vous avez une com­pré­hen­sion de base de ce qu'est asyn­cio, par­cou­rons cette nou­velle ver­sion du code de l'exemple et voyons com­ment il fonc­tionne. Notez que cette ver­sion ajoute aiohttp. Vous devrez exé­cu­ter pip ins­tall aiohttp avant de l'exécuter :

Cette ver­sion est un peu plus com­plexe que les deux pré­cé­dentes. Elle a une struc­ture simi­laire, mais il y a un peu plus de tra­vail pour confi­gu­rer les tâches que pour créer le Thread­Poo­lExe­cu­tor. Com­men­çons par le début de l'exemple.

download_​site()

download_​site() en haut est presque iden­tique à la ver­sion de threa­ding à l'exception du mot-clé async sur la ligne de défi­ni­tion de la fonc­tion et du mots-clés async with lorsque vous appe­lez réel­le­ment session.get().

download_​all_​sites()

download_​all_​sites() est l'endroit où vous ver­rez le plus grand chan­ge­ment par rap­port à l'exemple de threa­ding.

Vous pou­vez par­ta­ger la ses­sion entre toutes les tâches, de sorte que la ses­sion est créée ici en tant que ges­tion­naire de contexte. Les tâches peuvent par­ta­ger la ses­sion parce qu'elles sont toutes exé­cu­tées sur le même thread. Il est impos­sible qu'une tâche puisse en inter­rompre une autre alors que la ses­sion est dans le mau­vais état.

Dans ce ges­tion­naire de contexte, il crée une liste de tâches à l'aide de asyncio.ensure_future(), qui se charge éga­le­ment de les lan­cer. Une fois que toutes les tâches sont créées, cette fonc­tion uti­lise asyncio.gather() pour gar­der le contexte de la ses­sion en vie jusqu'à ce que toutes les tâches soient ter­mi­nées.

Le code de threa­ding fait quelque chose de simi­laire à ceci, mais les détails sont trai­tés de manière pra­tique dans le Thread­Poo­lExe­cu­tor. Il n'y a actuel­le­ment pas de classe Asyn­cio­Poo­lExe­cu­tor.

Il y a cepen­dant un petit, mais impor­tant, chan­ge­ment enfoui dans les détails ici. Rap­pe­lez-vous, nous avons par­lé du nombre de threads à créer. Il n'était pas évident de déter­mi­ner, dans l'exemple de mul­ti­threa­ding, quel était le nombre opti­mal de threads.

L'un des avan­tages de l'asyncio est qu'il s'adapte beau­coup mieux que le mul­ti-threa­ding. Chaque tâche prend beau­coup moins de res­sources et moins de temps à créer qu'un thread, donc la créa­tion et l'exécution d'un plus grand nombre d'entre-elles fonc­tionne bien. Cet exemple ne fait que créer une tâche sépa­rée pour chaque site à télé­char­ger, ce qui fonc­tionne très bien.

_​_​main_​_​

Enfin, la nature de l'asyncio signi­fie que vous devez démar­rer la boucle d'événements et lui indi­quer les tâches à exé­cu­ter. La sec­tion _​_​main_​_​ en bas du fichier contient le code pour obtenir_​event_​loop() et ensuite exécuter_​until_​complete().

Si vous avez mis à jour Python 3.7, les déve­lop­peurs du noyau Python ont sim­pli­fié cette syn­taxe pour vous. Au lieu de asyncio.get_event_loop().run_until_complete() tongue-twis­ter, vous pou­vez sim­ple­ment uti­li­ser asyn​cio​.run().

Pourquoi la version asyncio est la meilleure

C'est vrai­ment rapide ! Dans les tests sur ma machine, c'était la ver­sion la plus rapide du code :

$ python io_async.py

Down­loa­ded 79 sites in 3.088017225265503 seconds

le dia­gramme de timing d'exécution parait assez simi­laire a celui de la ver­sion mul­ti-threa­dée sauf que c'est le même thread qui fait toutes requêtes E/​S.

L'absence d'un joli wrap­per comme le Thread­Poo­lExe­cu­tor rend ce code un peu plus com­plexe que l'exemple de threa­ding. C'est un cas où vous devez faire un peu de tra­vail sup­plé­men­taire pour obte­nir de bien meilleures per­for­mances.

De plus, il y a un argu­ment cou­rant selon lequel le fait d'ajouter async et await dans les bons endroits est une com­pli­ca­tion sup­plé­men­taire. Dans une cer­taine mesure, c'est vrai. Le revers de la médaille de cet argu­ment est qu'il vous force à réflé­chir au moment où une tâche don­née sera per­mu­tée, ce qui peut vous aider à mieux orga­ni­ser votre code.

Les problèmes avec la version asyncio

Le pro­blème de la mise à l'échelle est éga­le­ment très impor­tant ici. L'exécution de l'exemple de threa­ding ci-des­sus avec un thread pour chaque site est sen­si­ble­ment plus lente que son exé­cu­tion avec une poi­gnée de threads. Exé­cu­ter l'exemple d'asyncio avec des cen­taines de tâches ne l'a pas ralen­ti du tout.

Il y a quelques pro­blèmes avec asyn­cio en ce moment. Vous avez besoin de ver­sions asyn­chrones spé­ciales des biblio­thèques pour pro­fi­ter plei­ne­ment de l'asyn­cio. Si vous aviez sim­ple­ment uti­li­sé requests pour télé­char­ger les sites, cela aurait été beau­coup plus lent car requests n'est pas conçues pour aver­tir la boucle d'événements qu'elle est blo­quée. Ce pro­blème est cor­ri­gé pro­gres­si­ve­ment quand de plus en plus de biblio­thèques adoptent l'asyn­cio.

Un autre pro­blème, plus sub­til, est que tous les avan­tages du mul­ti­tâche coopé­ra­tif dis­pa­raissent si l'une des tâches ne coopère pas. Une erreur mineure dans le code peut pro­vo­quer l'exécution d'une tâche et le blo­cage du CPU pen­dant une longue période, empê­chant d'autres tâches d'être exé­cu­tées. Rap­pe­lez-vous, il n'y a aucun moyen pour la boucle d'événements d'interrompre une tâche qui ne lui retourne pas le contrôle.

Dans cette optique, adop­tons une approche radi­ca­le­ment dif­fé­rente de la concur­rence, le mul­ti-trai­te­ment.

Version multiprocessing

Contrai­re­ment aux approches pré­cé­dentes, la ver­sion mul­ti­pro­ces­sée du code tire plei­ne­ment par­ti des mul­tiples CPU de l'ordinateur.

Le code de cet exemple est beau­coup plus court que l'exemple d'asyn­cio et res­semble beau­coup à l'exemple de threa­ding, mais avant de nous plon­ger en détail dans le code, fai­sons un petit tour rapide du mul­ti­pro­ces­sing.

Le multiprocessing en quelques phrases

Jusqu'à pré­sent, tous les exemples de concur­rences de cet article ne fonc­tionnent que sur un seul CPU dans votre ordi­na­teur. Ceci est lié à la concep­tion actuelle de CPy­thon et à ce qu'on appelle le Glo­bal Inter­pre­ter Lock, ou GIL.

Cet article ne se pen­che­ra pas sur le com­ment et le pour­quoi du GIL. Il suf­fit pour l'instant de savoir que les ver­sions syn­chrone, threa­ding et asyn­cio de cet exemple fonc­tionnent toutes sur un seul CPU.

mul­ti­pro­ces­sing dans la biblio­thèque stan­dard a été conçu pour bri­ser cette bar­rière et exé­cu­ter votre code sur plu­sieurs CPU. À un haut niveau, il le fait en créant une nou­velle ins­tance de l'interpréteur Python pour s'exécuter sur chaque CPU et en sous-trai­tant ensuite une par­tie de votre pro­gramme pour l'exécuter.

Comme vous pou­vez l'imaginer, créer un inter­pré­teur Python sépa­ré n'est pas aus­si rapide que lan­cer un nou­veau thread dans l'interpréteur Python actuel. Il s'agit d'une opé­ra­tion lourde qui com­porte des res­tric­tions et des dif­fi­cul­tés, mais selon le pro­blème, elle peut faire une énorme dif­fé­rence.

Le code de la version multiprocessée

Le code a quelques petits chan­ge­ments par rap­port à notre ver­sion syn­chrone. Le pre­mier est dans download_​all_​sites(). Au lieu d'appeler sim­ple­ment download_​site() à plu­sieurs reprises, il crée un objet multiprocessing.Pool et lui fait map­per download_​site vers les sites ité­rables. Cela devrait vous sem­bler fami­lier dans l'exemple de threa­ding.

Ce qui se passe ici, c'est que le Pool crée un cer­tain nombre de pro­ces­sus d'interpréteurs Python dis­tincts et va sur cha­cun, exé­cu­ter la fonc­tion spé­ci­fiée sur cer­tains des élé­ments de la liste des sites (de l'itérable plus géné­ra­le­ment). La com­mu­ni­ca­tion entre le pro­ces­sus prin­ci­pal et les autres pro­ces­sus est assu­rée pour vous par le module mul­ti­pro­ces­sing.

La ligne qui crée le Pool mérite votre atten­tion. Tout d'abord, il ne pré­cise pas le nombre de pro­ces­sus à créer dans le Pool, bien qu'il s'agisse d'un para­mètre facul­ta­tif. Par défaut, multiprocessing.Pool() déter­mi­ne­ra le nombre de pro­ces­seurs dans votre ordi­na­teur et le fera cor­res­pondre. C'est sou­vent la meilleure réponse, et c'est ici le cas.

Pour ce pro­blème, l'augmentation du nombre de pro­ces­sus n'a pas accé­lé­ré les choses. En fait, cela les a même ralen­ti car le coût de créa­tion et d’arrêt des pro­ces­sus était plus éle­vé que l'avantage de faire les demandes d'E/S en paral­lèle.

Ensuite, nous avons la par­tie initialiser=set_global_session de cet appel. Rap­pe­lez-vous que chaque pro­ces­sus de notre Pool a son propre espace mémoire. Cela signi­fie qu'ils ne peuvent pas par­ta­ger des choses comme un objet Ses­sion. Vous ne vou­lez pas créer un nou­veau dos­sier à chaque fois que la fonc­tion est appe­lée, vous vou­lez en créer un pour chaque pro­ces­sus.

Le para­mètre de fonc­tion de l'ini­tia­li­ser n'est conçu que pour ce cas. Il n'y a pas moyen de ren­voyer une valeur de retour de l'initialiser à la fonc­tion appe­lée par le pro­ces­sus download_​site(), mais vous pou­vez ini­tia­li­ser une variable de ses­sion glo­bale pour main­te­nir la ses­sion unique pour chaque pro­ces­sus. Parce que chaque pro­ces­sus a son propre espace mémoire, le glo­bal pour cha­cun sera dif­fé­rent.

C'est vrai­ment tout ce qu'il y a à faire. Le reste du code est assez simi­laire à ce que vous avez vu aupa­ra­vant.

Quelles sont les avantages de la version multiprocessée

La ver­sion mul­ti­pro­ces­sée de cet exemple est géniale parce qu'elle est rela­ti­ve­ment facile à mettre en place et néces­site peu d'options. Elle tire éga­le­ment plei­ne­ment par­ti de la puis­sance du pro­ces­seur de votre ordi­na­teur. Le dia­gramme de temps d'exécution de ce code res­semble à ceci :

Les problèmes avec la version multiprocessée

Cette ver­sion de l'exemple néces­site une confi­gu­ra­tion sup­plé­men­taire, et l'objet de ses­sion glo­bal est étrange. Vous devez pas­ser un peu de temps à réflé­chir aux variables qui seront acces­sibles dans chaque pro­ces­sus.

Enfin, il est net­te­ment plus lent que les ver­sions asyn­chrone et mul­ti­threa­dée dans cet exemple.

Ce n'est pas sur­pre­nant, car les pro­blèmes limi­tés par les E/​S ne sont pas vrai­ment la rai­son d'être du mul­ti­pro­ces­sing. Vous en ver­rez d'autres au fur et à mesure que vous pas­se­rez à la sec­tion sui­vante et que vous regar­de­rez des exemples liés au CPU.

Mer­ci aux relec­teurs Kevin Gueu­ti, Gwe­naelle, Plopp et Yoann M.



Pour continuer la lecture :


Commentaires

Laisser un commentaire