[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 ! 
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 :

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 :

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.

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âŠ

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 :

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 !