Thread vs Parallélisme

[new:30/03/2014]Beaucoup n’ont pas encore inflĂ©chi leur style de programmation vers le multitĂąche pourtant devenu indispensable. Certains l’ont fait et pensent que jouer avec les Threads est suffisant. En rĂ©alitĂ© le Threading n’est pas forcĂ©ment Ă©quivalent Ă  du parallĂ©lisme. Faisons le point !

Monotñche, mutitñche, parallùlisme


Il existe plusieurs façons de faire tourner un code. Dans sa version la plus simple c’est le mode monotĂąche qui est utilisĂ©. Peu importe le niveau technologique de l’ordinateur faisant tourner ce code, le dĂ©veloppeur se concentre sur l’écriture d’un code linĂ©aire, exactement comme on le faisait avec l’assembleur Z80 il y a de cela bien longtemps.

Dans la pratique la machine ne consacrera qu’un seule cƓur Ă  l’exĂ©cution d’un tel code et encore sera-t-il peut-ĂȘtre partagĂ© entre plusieurs processus diffĂ©rents. Autant dire que les performances ne seront pas aux rendez-vous.

Une autre façon de faire du tourner du code est d’utiliser le multitĂąche. Ici on utilise des classes comme System.Threading.Thread par exemple. Mais un Thread n’est jamais que du partage de temps sur un cƓur rien de plus
 Il faut en exĂ©cuter plusieurs Ă  la fois sur un OS qui sait les distribuer sur les diffĂ©rents cƓurs pour obtenir le rĂ©sultat escomptĂ©. Et cela beaucoup de dĂ©veloppeurs l’oublient


Enfin, la façon la plus moderne de faire tourner un code, moderne non pas par gout excessif d’une certaine modernitĂ© creuse et vide de sens basĂ©e sur l’apparence mais moderne parce que plus efficace, est d’utiliser le parallĂ©lisme. Ici le code sera exĂ©cutĂ© simultanĂ©ment sur plusieurs cƓurs pour exploiter au mieux les capacitĂ©s de la machine.

Loin est mon intention ici de faire un cours dĂ©taillĂ© sur tout cela. Mon objectif est de rappeler au lecteur la diffĂ©rence essentielle entre ces trois modes d’exĂ©cution et surtout d’éviter comme je le vois trop souvent qu’ils prennent des vessies pour lanternes c’est Ă  dire du threading pour du parallĂ©lisme


Threading <> Parallelisme

La confusion la plus terrible qu’on puisse voir ces derniers temps est celle qui est faite entre programmation multitĂąche utilisant des Threads et programmation parallĂšle utilisant d’autres procĂ©dĂ©s bien plus sophistiquĂ©s.

Certes est-il possible d’exploiter les cƓurs d’une machine en jouant avec des threads. Mais encore faut-il savoir combien de cƓurs expose-t-elle
 Car lancer 4 threads sur une machine dual core n’a que peu de sens, chaque cƓur fera tourner deux threads et passera ainsi une partie non nĂ©gligeable de son temps Ă  effectuer ce qu’on appelle du “time slicing” et du “context switching”. Entendez simplement par lĂ  que devant exĂ©cuter plusieurs tĂąches Ă  la fois (ce qui n’est pas possible), chaque cƓur tentera de simuler la simultanĂ©itĂ© en exĂ©cutant chaque thread l’un aprĂšs l’autre en alternance, cette derniĂšre Ă©tant assez rapide pour tromper l’humain et lui donner l’impression de la simultanĂ©itĂ©.

Mais il ne s’agit que de “time slicing”, de partage de temps, de dĂ©coupage de temps. De charcutage de temps. Et quand on coupe les cheveux en quatre, on n’obtient pas quatre cheveux mais le mĂȘme cheveu en quatre morceaux quatre fois plus petits que l’original
 On ne gagne donc rien en termes de performance. On peut gagner en impression de fluiditĂ©, mais ce n’est pas ce que nous cherchons ici.

Passer d’un thread Ă  l’autre n’est pas un job si facile pour un processeur (ou un cƓur de processeur multi-cƓur). Il lui en effet mĂ©moriser le contexte d’exĂ©cution du thread qui va ĂȘtre abandonnĂ© avant de passer au suivant afin de pouvoir recharger ce contexte pour reprendre le premier thread
 C’est le “context switching”. Les fondeurs ont certes fait de gros progrĂšs dans l’implĂ©mentation de ces possibilitĂ©s dans leurs puces. Mais malgrĂ© tous les efforts, 1+1 fait toujours deux, voire mĂȘme un peu plus, mais jamais moins ! Un peu plus car le temps d’exĂ©cution de deux threads sur un mĂȘme cƓur est augmentĂ© du temps des context switching
 De faits exĂ©cuter deux threads identiques sur un mĂȘme cƓurs ne durera pas deux fois plus longtemps mais lĂ©gĂšrement plus.

On perd du temps, on n’en gagne jamais à ce jeu là donc


Le parallĂ©lisme lui est basĂ© sur une autre approche : on sait combien il y a de cƓurs disponibles et on essaye de les charger au maximum (mais pas trop) en dĂ©coupant habilement un code Ă  exĂ©cuter pour que chaque “tranche” de ce dernier puisse s’exĂ©cuter indĂ©pendamment. Si on dispose de bons algorithmes pour dĂ©couper le code original et de “n” cƓurs disponibles il est donc possible de diviser le temps d’exĂ©cution du code original par “n”.

Bien entendu dans ce mode parallĂšle il y a aussi un peu de gestion Ă  prĂ©voir, ce qui consommera du temps. On ne divisera donc pas rĂ©ellement le temps initial par “n”, la rĂ©alitĂ© sera lĂ©gĂšrement en dessous. Mais plus “n” est grand, plus le gain est faramineux !

.NET et les tĂąches

Le Framework .NET a su au fil du temps s’amĂ©liorer dans de telles proportions qu’on se demande bien quel besoin il y aurait de crĂ©er une nouvelle plateforme. Ceci explique peut-ĂȘtre l’engouement modĂ©rĂ© des dĂ©veloppeurs pour WinRT. Quand on a dĂ©jĂ  ce qui se fait de mieux, pourquoi aller chercher plus loin


Parmi les amĂ©liorations que .NET a su porter depuis sa crĂ©ation on trouve tout un ensemble d’ajouts liĂ©s au multitĂąches et au parallĂ©lisme.

La notion de Thread, de ThreadPool, de Lock, etc, existent dĂ©jĂ  depuis longtemps. Mais d’autres modes ont Ă©tĂ© ajoutĂ©s pour traiter plus spĂ©cifiquement du parallĂ©lisme. C’est notamment la fameuse TPL, Task Parallism Library.

Cette bibliothĂšque de code est basĂ©e non plus sur le concept de Thread mais sur celui de Task (tĂąche) qui reprĂ©sente une opĂ©ration asynchrone. D’un certain point de vue les tĂąches ressemblent bien entendu aux Threads ou aux ThreadPools mais en se situant Ă  un niveau d’abstraction bien supĂ©rieur.

La TPL a d’abord Ă©tĂ© prĂ©sentĂ©e comme une librairie Ă  part, longtemps en test (la CPT des Parallel FX Ă©tait dĂ©jĂ  disponible en 2008). D’oĂč son nom de “TPL” avec un L comme Library. Un ajout donc. Mais Ă  partir de .NET 4.0 cette bibliothĂšque a Ă©tĂ© intĂ©grĂ©e au framework. Il ne s’agit plus d’un ajout plus ou moins expĂ©rimental mais bien du Framework .NET lui-mĂȘme !

Cet ensemble se divise en deux parties, PLINQ (Parallel Linq) qui ajoute la parallĂ©lisation aux requĂȘtes LINQ et la Task Paralel Library qui s’occupe plus directement du parallĂ©lisme au sein du code traditionnel.

Bien que cet ajout fut essentiel, peu de dĂ©veloppeurs se sont intĂ©ressĂ©s Ă  PLINQ et TPL. C’est un tort !

Et sans entrer dans un grand cours acadĂ©mique, et comme je l’indiquais plus haut, mon ambition du jour est fort humble : juste vous rappeler l’existence de tout cela et vous montrer rapidement par l’exemple les principales diffĂ©rences entre tout ces modes d’exĂ©cution.

J’avais dĂ©jĂ  abordĂ© le sujet de façon plus ou moins directe dans quelques billets, il s’agit donc d’en remettre une petite couche pour vous inciter Ă  regarder tout cela de plus prĂšs. A force j’y arriverais !

Pour information vous trouverez sur Dot.Blog :

 

Cela fait donc environ 4 ans que rĂ©guliĂšrement je viens sonner la petite cloche du parallĂ©lisme pour attirer votre attention
 Certains l’ont bien entendu teinter, pour d’autres j’espĂšre que cette fois-ci son son mĂ©lodieux arrivera Ă  vos dĂ©licates oreilles pour remonter votre nerf auditif et enfin rĂ©veiller certains neurones qui devraient dĂ©jĂ  bosser sur le sujet depuis un moment ! Sourire

Un exemple simple

J’aime les exemples, ils parlent souvent mieux que de longs discours. Et les exemples simples sont ceux que je prĂ©fĂšre par dessus tout


Pour vous faire sentir la diffĂ©rence entre tous les modes d’exĂ©cution Ă©voquĂ©s ici, je vous propose ainsi un petit exĂ©cutable en mode console qui va utiliser trois façons diffĂ©rentes de faire tourner la mĂȘme sĂ©quence. Chaque mode sera chronomĂ©trĂ© et j’accompagnerai l’exĂ©cution de chacun d’un clichĂ© issu du moniteur de performance de Windows pour que vous puissiez voir la diffĂ©rence d’occupation des cƓurs.

Le principe

J’ai dit simple
 Donc une routine toute bĂȘte qui s’amuse Ă  ajouter un million de fois un “x” Ă  une chaĂźne de caractĂšres. De la façon la plus bĂȘte, la moins subtile qu’il soit histoire que cela prenne assez de temps pour mesurer le temps d’exĂ©cution et afficher PERFMON de Windows, le remettre Ă  zĂ©ro et lancer CAPTURE pour prendre un clichĂ© de la fenĂȘtre


Le visuel

On est loin de mes grands discours sur l’UI et l’UX ici ! Un simple projet console avec un menu “à l’ancienne” comme on le faisait il y a 30 ans :

image

On dispose donc de trois choix, le monde standard, le mode threadé et le mode parallÚle (plus une possibilité de quitter le programme).

Commençons par le commencement


Mode Standard

Le mode standard c’est la “programmation à papa”. Je fais une boucle FOR et j’ajoute un million de fois un “x” à la chaüne de caractùres.

Sin on fait abstraction de la non utilisation d’un StringBuilder (c’est fait exprĂšs pour que le test dure plus longtemps), c’est un code simple, comme on en voit partout, donc 99% du code Ă©crit encore aujourd’hui :

        public static void Run1Million()
        {
            var s = "";
            for (var i = 0; i < 1000000; i++)
                s = s + "x";
        }

 

J’avais prĂ©venu, c’est pas de la haute voltige !

Le moniteur des performances non montre quelque chose de ce genre durant l’exĂ©cution de ce merveilleux bout de code :

image

La machine Ă©tant occupĂ©e Ă  d’autre petites choses (musique, camĂ©ras de surveillance, etc), ses huit coeurs ne sont pas totalement au repos, le bleu et le violet bossent Ă  mi-temps sans trop se fouler, les autres roupilles au fond du diagramme, et on voit le cƓur 0 qui s’agite tout seul (le trait rouge) faisant un travail pas trop fatigant (jamais il ne monte Ă  100%) mais constant alors que tout le monde se roule les pouces. Le garbage collector de .NET doit ĂȘtre responsable d’un des deux autres threads qui travaillent un peu, car ma sĂ©quence oblige Ă  crĂ©er 1 million de string qui sont abandonnĂ©es Ă  chaque fois (les string sont immuables en .NET, rappelez-vous
 faire “x=x+”y”” oblige en fait Ă  crĂ©er une nouvelle string x et Ă  disposer l’ancienne. On s’imagine bien Ă  quel point cela peut stresser le GC !).

Ce diagramme des performances c’est un peu comme dans notre mĂ©tier, il y a un dĂ©veloppeur stagiaire qui bosse, un chef de projet qui discute Ă  la machine Ă  cafĂ©, un directeur de projets qu’on cherche car il doit ĂȘtre ‘quelque part’ dans le bĂątiment, un DSI qui est ‘à l’extĂ©rieur’ et un big boss qui est au golf.

Au bout de 5 minutes 27 et quelques millisecondes dont je vous fais grĂące, ce manĂšge d’esclavagiste prend fin et le cƓur 0 peut enfin prend un repos mĂ©ritĂ© sous les yeux rĂ©probateurs des autres qui se disent que ce n’est pas normal qu’un stagiaire sortent des bureaux aussi “tĂŽt” - mĂȘme s’il est dĂ©jĂ  21h45.

L’efficacitĂ© de notre programme est Ă  l’exemple de celle des sociĂ©tĂ©s qui fonctionnent comme ma petite caricature : elle est nulle.

Mode threadé

ArmĂ© de bonnes intentions le stagiaire veut faire voir qu’il a bossĂ© un peu, et il se dit qu’il va utiliser un thread pour amĂ©liorer les choses.

Ce qui donne ce code-ci :

        public static void Run1MillionInThread()
        {
            var t = new Thread(Run1Million);
            t.Start();
        }

Un thread est créé et activé pour exécuter le code précédent.

image

On a bien gagnĂ© en “fluiditĂ©â€, le thread principal de notre programme console a Ă©tĂ© libĂ©rĂ© tout de suite et affiche dĂ©jĂ  les rĂ©sultats alors que le travail vient Ă  peine de commencer
 Bien entendu la durĂ©e affichĂ©e est fausse.

Quant au moniteur de performances


image

 

Avant le lancement de la mĂ©thode, tous les cƓurs sont au repos, au moment de l’activation c’est le grand branle-bas de combat tout le monde s’affole, et ensuite on obtient un diagramme trĂšs proche de l’exĂ©cution prĂ©cĂ©dente, c’est Ă  dire un cƓur qui travaille (le bleu) et les autres qui flemmardent.

Bref, monotĂąche ou threading, ça revient au mĂȘme point de vue performances. Ca serait presque pire avec le code exĂ©cutĂ© dans un thread. Le seul gain vĂ©ritable ici est d’avoir libĂ©rĂ© le thread principal ce qui permet Ă  la fenĂȘtre de notre application de rester fonctionnelle et rĂ©active. C’est dĂ©jĂ  pas mal. Mais ce n’est pas ce qu’on vise ici. Dommage.

Mais si on vise la performance pure, il faudrait dĂ©couper notre boucle de 1 million en plusieurs threads exĂ©cutĂ©s en mĂȘme temps sur des cƓurs diffĂ©rents puis concatĂ©ner le rĂ©sultat. Ca va devenir du travail Ă  Ă©crire tout ça !!!

Le mode parallĂšle

Heureusement il n’y aura rien Ă  Ă©crire, en tout cas pas de ce genre lĂ . En utilisant les possibilitĂ©s du Framework .NET, nous allons profiter des algorithmes de ce dernier pour Ă©crire quelque chose de fort simple mais de redoutablement efficace


Le code ressemble maintenant Ă  celui-lĂ  :

         public static void Run1MillionParallel()
        {
            var s = "";
            Parallel.For(0, 1000000, x => s = s + "x");
        }

 

C’est trùs court et trùs efficace comme vous allez le voir.

Redoutablement efficace
 Le moniteur de performances nous montre ceci :

image

Huit cƓurs qui dĂ©marrent et qui bossent enfin ! Tous unis pour rĂ©soudre un mĂȘme problĂšme avec comme volontĂ© de le faire le plus vite possible.

Le chrono est sans appel : 24 secondes et quelques millisecondes.

24 secondes au lieu de 5 minutes et 27 secondes pour le code standard !!!

24 au lieu de 327 secondes
 13, 625 fois moins de temps alors que n’utilisons pas 14 cƓurs mais seulement 8 qui sont malgrĂ© tout un peu occupĂ© Ă  tout le reste (comme je le disais, musique, camĂ©ras, et plein d’autres petites choses) !

Conclusion

Comme je l’ai annoncĂ©, ce billet ne sera pas un cours sur TPL ou le threading, il existe une tonne de docs sur le sujet et je pense en plus que j’y reviendrais en dĂ©tail prochainement tellement je le pense nĂ©cessaire.

Jute un rappel : 24 secondes au lieu de 327, pour un simple Ă©change dans notre code d’une boucle for classique par une Parallel.For().

C’est à vous de voir


Mais Stay Tuned !