1 / 40

La programmation concurrente

La programmation concurrente. On est rendu là !. par Michel Michaud, juin 2011 http://www.michelmichaud.com. Petit quiz. Un programme simple un peu idiot :. int main() { time_t départ = time(0); long cptPairs = 0; long cptImpairs = 0;

tehya
Download Presentation

La programmation concurrente

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. La programmation concurrente On est rendu là ! par Michel Michaud, juin 2011 http://www.michelmichaud.com

  2. Petit quiz Un programme simple un peu idiot : int main() { time_t départ = time(0); long cptPairs= 0; long cptImpairs = 0; for (long i = 2; i <= 2000000000; ++i) { if (i % 2 == 0) ++cptPairs; else ++cptImpairs; } cout << "Durée " << difftime(time(0), départ) << " s\n" << "Pairs : " << cptPairs << ", impairs : " << cptImpairs << '\n'; }

  3. Résultat de l'exécution • Sur un Pentium IV (2002) : Durée 6 s [moyenne de 100 exécutions : 6.33 s] Pairs : 1000000000, impairs : 999999999 • Sur un Pentium MMX (1997) : Durée 138 s Pairs : 1000000000, impairs : 999999999 • Sur un 486SX (1991) : Durée 1663 s Pairs : 1000000000, impairs : 999999999 QUESTION : sur un i7 860 (2009) : Durée ????moyenne de 100 exécutions : a) 0.67 b) 2.4 c) 6.8 d) 12.1 Pairs : 1000000000, impairs : 999999999

  4. Résultat de l'exécution • Sur un Pentium IV (2002) : Durée 6 s [moyenne de 100 exécutions : 6.33 s] Pairs : 1000000000, impairs : 999999999 • Sur un Pentium MMX (1997) : Durée 138 s Pairs : 1000000000, impairs : 999999999 • Sur un 486SX (1991) : Durée 1663 s Pairs : 1000000000, impairs : 999999999 QUESTION : sur un i7 860 (2009) : Durée 6smoyenne de 100 exécutions : a) 0.67 b) 2.4 c) 6.8 d) 12.1 Pairs : 1000000000, impairs : 999999999

  5. Résultat de l'exécution (résumé) • Sur un 486SX 33 MHz (1991) Durée 1663 s • Sur un Pentium MMX 233 MHz (1997) Durée 138 s • Sur un Pentium IV 2.4 GHz (2002) Durée 6 s • Sur un i7 860 2.8 GHz (2009) Durée 6 s • Estimation pour i7 990 3.4 GHz (2011) Durée 5 s • Même si on atteint 5 GHz dans la décennie (improbable sur un micro-ordinateur), ça ne donnerait que 3 s. MAIS POURTANT…

  6. Pourtant aujourd'hui même… • En modifiant le programme assez simplement, pour profiter des multiples cœurs d'un simple i7 860 actuel (2.8 GHZ), on peut obtenir :Durée 2 s Pairs : 1000000000, impairs : 999999999 • Ou même, encore plus simplement, en C#/.NET, toujours avec le i7 860 :Durée 00:00:00.6749000 (i.e. 0.67 s) Pairs : 1000000000, impairs : 999999999 • Est-ce qu'on peut se passer de ces techniques ?

  7. Un article important « The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software » parHerbSutter (2005) http://www.gotw.ca/publications/concurrency-ddj.htm • Seule l'augmentation de la mémoire cache des UCT aura un impact automatique sur les programmes. • Profiter des autres améliorations ne sera pas gratuit, les programmes qui n'utilisent aucun parallélisme devront être modifiés, si possible, pour en profiter. • Les programmeurs devront (re)commencer à se préoccuper d'efficacité. • Les langages de programmation devront permettre explicitement la concurrence/parallélisme.

  8. Les conclusions importantes pour nous • La POO a été la dernière modification importante dans les paradigmes de programmation. Nous avons modifié nos enseignements, parce que l'on voulait profiter de ce qu'elle offrait. • La programmation concurrente (on dit parfois programmation parallèle) est la prochaine modification à considérer. • En fait, on est probablement en retard. • Tout comme la POO, ça pourrait tout changer dans l'apprentissage de la programmation.

  9. Un exemple pas si complexe (1) • On a tous l'habitude de parler des boucles (ou structure itérative) et des structures de contrôle des langages qui sont associées. • Maintenant, dès le départ ou presque, on pourrait trouver fondamental de différencier : • les boucles à compteur (avec indice d'itération); • les boucles représentant des traitements à faire en séquence (une étape après l'autre); • les boucles représentant des traitements qui pourraient se faire en parallèle. • Symboliquement : for/foreach/forall…

  10. Un exemple pas si complexe (2) • Si le langage le supporte, peut-on vraiment éviter de présenter/différencier : // Le classique for (int i = 0; i != clients.size(); ++i) soldeTotal += clients[i].SoldeÉpargne() + clients[i].SoldeCommercial(); // Le moderne foreach (Client c in clients) soldeTotal += c.SoldeÉpargne() + c.SoldeCommercial(); // L'idéal ? forall (Client c in clients) soldeTotal += c.SoldeÉpargne() + c.SoldeCommercial(); (N.B. L'exemple est en pseudo C#/Java/C++, mais l'exemple est très près de ce qui existe vraiment...)

  11. Les difficultés pour l'enseignement • Coder des applications impliquant du parallélisme n'est pas facile. • Les tester l'est encore moins. • Ce ne sont pas toutes les applications qui peuvent profiter du parallélisme, mais ça arrive autant dans les applications simples que dans les complexes. • Ce n'est donc pas quelque chose à enseigner nécessairement après tout le reste. • Tout ce qui précède pouvait s'appliquer à la POO, il n'y a pas si longtemps ! On a réussi (je crois) ce tournant. Il y a de l'espoir donc, mais il faut de nouveau faire face au changement.

  12. Comprendre le parallélisme (1) • Il faut un système d'exploitation supportant la multiprogrammation : • plusieurs programmes « en mémoire » au même moment : ce sont les processus; • ces processus peuvent réellement s'exécuter en parallèle, indépendamment, s'il a plusieurs unités centrales de traitement; • sinon le système d'exploitation partage l'utilisation de l'UCT entre les divers processus prêt à s'exécuter (tranche de temps); • même s'il n'y a qu'une UCT, donc pas de vrai parallélisme, on considère qu'il y a du parallélisme, car dans un intervalle de temps donné, plusieurs processus ont avancé dans leur exécution; • chaque processus pense donc qu'il est le seul à s'exécuter, mais sur une machine un peu plus lente; • la multiprogrammation est généralement transparente/invisible aux processus. • Presque tous les systèmes actuels supportent la multipro-grammation (plus rarement dans les systèmes embarqués).

  13. Comprendre le parallélisme (2) • Deux concepts s'affrontent pour faire une application profitant du parallélisme : celui du processus et celui du fil d'exécution (thread). • Un fil d'exécution est l'idée qu'à un moment précis l'exécution d'un processus est rendue à un certain point dans un certain contexte. Les actions antérieures ont déterminé le contexte actuel (état des fichiers ouverts, valeurs des registres, etc.) qui, lui, déterminera le sens exact de la suite des actions. • Le système d'exploitation doit conserver toutes les informations des contextes afin de pouvoir les rétablir quand il passe le contrôle d'un processus à l'autre; en fait, on parlera d'ailleurs plutôt de changement de contexte (contextswitching). • On peut construire des applications exploitant le parallélisme en mettant en jeu plusieurs processus (provenant habituellement de programmes différents) qui collaborent. • On peut aussi proposer au système d'exploitation d'avoir plusieurs fils d'exécution différents associés au même processus !

  14. Comprendre le parallélisme (3) • Un programme est dit multifil (multi-threaded) s'il est conçu pour que plusieurs de ses parties puissent s'exécuter en même temps. • Pour le système d'exploitation, ça ne change pas grand-chose que les contextes conservés proviennent de processus différents ou non. • On n'a pas toujours le choix entre une application multipro-cessus et une application multifil : dans certains cas, une technique plutôt que l'autre est essentielle ou préférable. • Les problèmes sont les mêmes, mais les solutions peuvent parfois être différentes. • Même les applications multiprocessus peuvent mettre en jeu des programmes multifil : il s'agit donc probablement de ce qui est le plus fondamental.

  15. Comprendre le parallélisme (4) • Au niveau pratique, le système d'exploitation doit fournir des outils permettant de créer un processus ou un fil. • Il doit aussi permettre leur intercommunication et leur synchronisation, sinon on n'aura que des processus indépendants, sans parallélisme utile. • Plusieurs approches sont possibles. Les systèmes d'exploitation, mais aussi des bibliothèques complémentaires, peuvent offrir des moyens variés, plus ou moins simples d'utilisation. • Dans certains cas, les langages vont aussi offrir des éléments pour simplifier l'écriture d'application exploitant le parallélisme (par exemple, C# et Java offrent certains mots clefs à cette fin).

  16. Applicabilité (1) • Pour certains « logiciels », le parallélisme est un besoin fondamental pour obtenir le résultat escompté. • Un exemple classique : l'impression en arrière-plan. • Une commande est donnée par l'utilisateur, par laquelle il indique quel fichier imprimer. (par exemple : $ lp fichier) • Ce programme se termine aussitôt pour redonner le contrôle à l'utilisateur. Donc c'est un autre qui conservera la liste des fichiers à imprimer; il doit demeurer en mémoire et communiquer au besoin avec les commandes des utilisateurs (qui pourraient demander une liste des fichiers en attente, etc.). • S'il s'occupe de cette communication, ce programme ne peut pas, en plus, faire l'impression (parler à l'imprimante) lui-même. Il faut donc un troisième programme pour ça… • Notez qu'un programme multifil pourrait peut-être remplacer les deux derniers, mais pas le premier.

  17. Applicabilité (2) • Dans certains cas, une application semble faire plusieurs choses à la fois. On peut alors espérer que d'utiliser le parallélisme sera plus simple que de le simuler. • Par exemple, un programme qui affiche une horloge mise à jour en temps réel (l'heure du jour ou un compte à rebours), pendant qu'il fait autre chose de plus « sérieux ». • Évidemment, on peut aussi simplement vouloir exploiter le parallélisme pour augmenter la performance. Il faudra alors chercher où l'appliquer, ce qui n'est pas nécessairement facile. • L'amélioration réelle de performance n'est possible que si les fils ou processus peuvent vraiment s'exécuter en même temps, donc sur des unités centrales indépen-dantes. C'est ça qui est « très » nouveau : tous les nouveaux microprocesseur ont plusieurs noyaux !

  18. Applicabilité (3) • Les applications client-serveur sont évidemment des applications multiprocessus, mais les problèmes (et les solutions) sont très différents des applications qui fonctionnent sur une seule machine. • Par contre, l'aspect multiutilisateurs peut rejoindre nos préoccupations, car il s'agit de parallélisme au niveau de la même machine (le serveur).

  19. Différences multiprocessus/multifil • Les processus indépendants doivent faire un effort pour se communiquer des informations. • Plusieurs outils pour ça peuvent être offerts par les systèmes d'exploitation, de l'envoi de messages bien structurés, dans une file d'attente, à la simple mémoire partagée. Mais seules les données choisies sont communiquées. • À l'opposé, un fil d'exécution d'un programme a généralement accès à toutes les données du programme accessibles aux autres fils du même programme. • Les idées générales de protection des données, comme l'encapsulation, nous pousseraient donc vers les applications multiprocessus, sauf quand les données à partager sont complexes, ce qui est malheureusement souvent le cas. • Le problème majeur du partage des données est celui des accès concurrents (« race condition » ou situation de compétition). Il sera généralement beaucoup plus problématique dans les programmes multifil, qui partagent des données par défaut, sans s'en rendre compte…

  20. En pratique, le partage des données (1) • Un processus peut demander au système d'exploitation de créer une zone de mémoire qui sera ensuite accessible par un ou plusieurs autres processus. Les processus ajustent un pointeur vers l'adresse de cette zone et peuvent ensuite y accéder normalement. • Ce qui est mis dans cette zone est automatiquement et instantanément visible à tous les processus. • Il faut absolument mettre en place un protocole de partage pour éviter les problèmes d'accès concurrents. • Cette solution est celle qui ressemble le plus au partage des données direct entre fils d'exécution dans le même processus, mais elle n'est pas réellement aussi simple, en particulier à cause de l'allocation dynamique.

  21. En pratique, le partage des données (2) • Dans les langages orientés objets, qui utilisent abondamment l'allocation dynamique, on ne peut pas partager des objets ainsi alloués entre les processus, même si on met leur adresse en mémoire partagée, leurs données, elles, ne seraient pas en mémoire partagée. • En C++, par exemple, on ne peut pas mettre, de façon utile, de std::string en mémoire partagée. Il faudrait se limiter aux vecteurs de caractères (char [n]). • Si le système d'exploitation permet l'envoi de messages (ou offre un système de boîtes aux lettres), on pourra éviter plus facilement la plupart des problèmes d'accès concurrents, mais d'autres difficultés apparaissent. • De toute façon, ça ne règle rien au niveau des objets alloués dynamiquement. • L'approche « partage total » du multifil a des attraits !

  22. Exemple de partage multiprocessus (1) • On décide des données à partager, par exemple avec une structure : structTypePartage { double totalDesVentes; intnoFactureÀCréer; char nomClient[100]; // etc. }; • Un des processus demande la création d'une zone de mémoire partagée pour cette structure et récupère son adresse. On utilise un identifiant, connu des processus impliqués, pour nommer, de façon globale, cette zone particulière : TypePartage* pMP = CréerMémoirePartagée(sizeof(TypePartage), "MP_A332133PPA"); • Le processus peut ensuite utiliser cette adresse normalement : pMP->totalDesVentes = 0.0; pMP->noFactureÀCréer = 1; strcpy(pMP->nomClient, "Michel");

  23. Exemple de partage multiprocessus (2) • Les autres processus demande simplement l'accès et utilise l'adresse reçue pour accéder aux données partagées : TypePartage* pMP = OuvrirMémoirePartagée("MP_A332133PPA"); if (pMP == NULL) Erreur("Oups, mémoire partagée non créée"); pMP->totalDesVentes += PréparerFacture(pMP->noFacture, pMP->nomClient); • Les problèmes (d'accès concurrent) classiques sont bien présents dans le code présenté… • Pour information, le code de CréerMémoirePartagée sous Windows : void* CréerMémoirePartagée(size_tp_taille, const char* p_nomMP) { HANDLE hMP = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, (DWORD)p_taille, p_nomMP); if (hMP == 0 || GetLastError() == ERROR_ALREADY_EXISTS) return NULL; return MapViewOfFile(hMP, FILE_MAP_ALL_ACCESS, 0, 0, 0); } • Oui, sous Windows le système de mémoire partagée se confond avec les fichiers mappés en mémoire (memory-mapped files)…

  24. Exemple de partage multiprocessus (3) • La même fonction, version Unix/Linux : void* CréerMemoirePartagée(size_tp_tailleMP, const char* p_nomMP) { key_tclefMP = ftok(p_nomMP, 1); if (clefMP == (key_t)-1) return NULL; intidMP = shmget(clefMP, p_tailleMP, IPC_CREAT|IPC_EXCL|0666); if (idMP == -1) return NULL; return shmat(idMP, NULL, 0); } • Ces fonctions sont des exemples montrant que le code est possible. Dans les discussions avec les élèves, c'est important d'arriver rapidement à des abstractions (ici « CréerMémoirePartagée » et « OuvrirMémoirePartagée »), car il y a encore beaucoup à dire avant d'arriver à la pratique…

  25. Exemple de création de processus • Grosso modo, on donne le nom du programme exécutable au système d'exploitation et on lui demande de l'exécuter. • Certains systèmes d'exploitation permettent de spécifier si le processus qui fait la demande veut attendre la fin du nouveau processus ou continuer en parallèle avec lui. • Sous Windows et Unix, la création est en parallèle, mais il est facile de programmer l'attente de la fin de l'exécution d'un processus. • Un exemple sous .NET (en fait, une des nombreuses variantes) : ProcessblocNotes = new Process(); blocNotes.StartInfo.FileName = @"C:\Windows\Notepad.exe"; blocNotes.StartInfo.Arguments = "fichier.txt"; blocNotes.Start();   // ... On fait ce qu'on veut (on est en parallèle) ... Console.WriteLine("Attente de la fin du Bloc-notes..."); blocNotes.WaitForExit(); • En comparaison, c'est un peu plus lourd avec Windows seul, et c'est un peu plus subtil sous Unix/Linux...

  26. Exemple de création de fil d'exécution • Grosso modo, on demande simplement au système d'exploitation de démarrer un fil correspondant au code d'une fonction. • Exemple symbolique Windows : TypeQuelconquemonParam = ...; // Des valeursqu'onveut passer en paramètre au fil DWORD idFil;   HANDLE hFil = CreateThread(NULL, 0, MaFonctionPourFil, &monParam, 0, &idFil); CloseHandle(hFil); // Sauf si on besoin du handle (rare) • La fonction pour le fil doit avoir une signature un peu spéciale : DWORD WINAPI MaFonctionPourFil(LPVOID p_params) { // p_paramspourraitêtreconverti en TypeQuelconque* pour accéder aux paramètres... // Ici on peut faire ce qu'on veut, ou presque...   return 0; // Ou autre si erreur... } • On verra un exemple complet en .NET plus loin.

  27. Le problème des accès concurrents (1) • Le système d'exploitation peut interrompre un processus n'importe quand pour donner le contrôle à un autre. Ceci peut survenir à un mauvais moment, dans ce qu'on appelle une section critique… • Symboliquement : Variable partagée : v 0 Processus 1 veut faire v = v + 1 Processus 2 veut faire v = v + 2 Exécution chanceuse : v P1a : Lire la valeur de v (0)  0 0 P1b : Calcul de la nouvelle valeur (0+1) 0 P1c : Écriture dans la variable (1) 1  1 P2a : Lire la valeur de v (1)  1 1 P2b : Calcul de la nouvelle valeur (1+2) 1 P2c : Écriture dans la variable (3) 3  3 OK ! Exécution malchanceuse : v P1a : Lire la valeur de v (0)  0 0 P1b : Calcul de la nouvelle valeur (0+1) 0 P2a : Lire la valeur de v (0)  0 0 P1c : Écriture dans la variable (1) 1  1 P2b : Calcul de la nouvelle valeur (0+2) 1 P2c : Écriture dans la variable (2) 2  2 Erreur ! • On doit s'en préoccuper !

  28. Le problème des accès concurrents (2) • L'exemple précédent peut aussi très bien s'appliquer à deux fils d'exécution dans le même processus, qui accède à une variable quelconque, partagée naturellement. • Ces cas, où la vitesse relative des processus peut être la cause d'un problème, sont appelés race condition en anglais, ou situation de compétition. • En fait, on peut aussi simplement parler du problème de l'exclusion mutuelle ou des accès concurrents : pendant un certain temps, la durée de la section critique en fait, certaines ressources ne doivent pas être complètement partagées. • La façon d'y parvenir n'est pas aussi simple qu'on pense. En fait, on aura normalement recours au support du système d'exploitation. • Le problème pour les débutants (et donc nos élèves) est que le programme peut fonctionner, par hasard, la plupart du temps. Une fois le problème détecté, on peut essayer de le régler, mais si on ne le voit pas ?

  29. Le problème des accès concurrents (3) • Présentation symbolique du problème et de la solution souhaitée. Plusieurs fils/processus ont des sections critiques devant être en exclusion mutuelle : for(;;) // Sortie de boucle non montrée, mais quelque part... { SectionNonCritique(); PréambuleSC(); // Préparation pour éviter le problème (peut bloquer certains processus) SectionCritique(); // Le code qui doit être en exclusion mutuelle QuitterSC(); // Finalisation permettant aux autres processus de passer en section critique } • On veut savoir quoi faire dans PréambuleSC() et QuitterSC(). • Par exemple, une solution simple, mais inacceptable, est de désactiver les interruptions dans PréambuleSC() et de les réactiver dans QuitterSC(). C'est souvent possible, mais la section critique pouvant être longue, on ne veut pas empêcher l'avancement des processus (ou fil) qui n'ont rien à voir avec cette section critique. • En fait, il y a plusieurs autres critères pour une bonne solution, entre autres, il ne faudrait pas que les processus fassent de l'attente active, c'est-à-dire boucle en attente, gaspillant ainsi du temps processeur qui pourrait permettre au processus en section critique de la quitter plus rapidement !

  30. Le problème des accès concurrents (4) • Un exemple de mauvaise solution classique : bool ok = true; // Partagée entre les processus/fils for(;;) { SectionNonCritique(); while (!ok) {} // On attend ok = false; // On bloque les autres SectionCritique(); ok = true; // On laisse passer les autres } • Le problème de cette « solution » n'est pas simplement l'attente active… • De nombreuses « solutions » du genre ont été proposées. Les solutions correctes de Decker et Peterson sont les plus célèbres, mais elles souffrent toutes les deux de l'attente active (c'est voulu). • La vraie solution est l'emploi des sémaphores et variantes...

  31. Sémaphores, mutex, lock, etc. (1) • Un sémaphore est un objet semblable à un compteur, mais qui est généralement sous le contrôle total du système d'exploitation. • On peut y appliquer deux opérations, qu'on peut symboliquement appeler Up et Down. • Le Down sur un sémaphore le fait décrémenter de 1 sauf s'il était à 0 : dans ce cas, le système met le processus demandeur (ou le fil) en état bloqué et le met en attente du sémaphore. • Le Up incrémente le compteur sauf s'il était à 0 et qu'il y a des processus en attente : dans ce cas, un des processus est débloqué et peut continuer son exécution. • Évidemment ces opérations se passent dans le système d'exploitation et sont considérées atomiques, donc ne pouvant être interrompues.

  32. Sémaphores, mutex, lock, etc. (2) • Les sémaphores permettent de résoudre plusieurs problèmes classiques en programmation concurrente. En particulier, on peut résoudre facilement le problème de l'exclusion mutuelle ainsi (symboliquement) : Sémaphore s = new Sémaphore(1); // Variable partagée // (avec compteur initialement à 1) for(;;) { SectionNonCritique(); s.Down(); SectionCritique(); s.Up(); } • Dans les applications multiprocessus, on utilise des sémaphores globaux au système, identifiés par un nom, un peu comme on a vu pour les zones de mémoire partagée. Les opérations Up et Down sont donc des appels systèmes et non de simples opérations sur des variables ordinaires. • Pour l'exclusion mutuelle, le sémaphore peut simplement être à 0 ou à 1. C'est un sémaphore binaire, offert spécifiquement par certains systèmes d'exploitation.

  33. Sémaphores, mutex, lock, etc. (3) • Dans certains systèmes, les sémaphores binaires sont appelés des mutex (de Mutual Exclusion), mais c'est aussi le nom ou préfixe classique pour les sémaphores qui servent à cette fin dans les exemples symboliques. • Sous .NET, on peut gérer des Mutex avec nom, globaux au système pour les applications multiprocessus, mais pour les fils, on utilise habituellement directement un objet partagé simple (sans nom). Par exemple : // Donnée membre d'une classe (pourrait être static) MutexmutexÉcritureLog = new Mutex(); // En fait, un sémaphore binaire avec init à 1 // Puis dans les fonctions (qui pourraient être des fils d'exécution parallèles) mutexÉcritureLog.WaitOne(); // C'est le Down ÉcrireLog(); mutexÉcritureLog.ReleaseMutex(); // C'est le Up • N.B. Bien sûr, il serait peut-être plus logique de mettre la protection par ce mutex dans la fonction ÉcrireLog elle-même, si c'est sa seule utilité.

  34. Sémaphores, mutex, lock, etc. (4) • En principe donc, les opérations sur les mutex viennent par paire. Il ne faut pas oublier de faire le Up lorsqu'on fait un Down. • Donc, si des exceptions peuvent survenir et faire sauter le Up, on a un problème. Selon le langage de programmation utilisé, des idiomes (destructeur/RAII en C++, finally en Java ou C#, using() en C#, etc.) pourraient nous aider à éviter les problèmes. • Mais l'idéal serait une certaine forme d'automatisation. C# en propose une…

  35. Sémaphores, mutex, lock, etc. (5) • Automatiser la protection des sections critiques est ce que fait l'instruction lock en C#. Pour identifier les sections critiques reliées, on doit simplement faire un lock sur un objet spécifique quelconque, souvent un objet tout simple, défini simplement pour ça. • Par exemple, au lieu du code précédent, on aurait pu faire : // Donnée membre d'une classe (pourrait être static) objectpourSynchroLog = new object(); // Puis dans les fonctions (qui pourraient être des fils d'exécution parallèles) lock (pourSynchroLog) { ÉcrireLog(); } • L'avantage est évidemment l'abstraction totale du concept. À la place, on a le résultat désiré, l'exclusion mutuelle, exprimée de façon claire. Pas besoin de parler de sémaphore, de mutex, d'exception, etc. En tout cas, pas pour ce problème ! • Ce n'est pas la seule solution « haut niveau » offerte par les langages de programmation courants…

  36. Les moniteurs • En terme de grand principes, à part ce dont on a déjà parlé, c'est celui du moniteur qui est le plus général/important/connu/différent. • Inventé par Brinch Hansen et Hoare en 1973 (!), ce sont toujours des variantes qu'on retrouve dans les systèmes d'exploitation et langages. • Le principe de base est d'avoir un collection de fonctions/procédures/méthodes qui sont automatiquement en exclusion mutuelle. • S'ajoute quelques opérations comme l'attente d'un condition, l'envoi de signal aux autres fils, etc. • En Java, on a les mots clefs synchronised, wait,notify, etc.qui implémentent une version simplifiée, mais suffisante, des moniteurs. • .NET offre aussi la notion de moniteurs. En fait, lock utilise la classe Monitor…

  37. Autres particularités de .NET • N'y a-t-il pas un danger de demander plus de fils ou de processus qu'il y a de cœurs dans notre UCT si la seule raison pour ceux-ci est la performance ? Si ceci vous préoccupe, la notion de thread pool vous intéressera. • Cependant, .NET veut nous faire dépasser le stade de la gestion manuelle des fils d'exécution et des thread pool, avec la notion de Task. À suivre… • À l'inverse, .NET offre des opérations atomiques simples préemballées. Par exemple, la classe Interlocked offrent des fonctions statiques faisant l'équivalent d'un ++ ou d'un +=, par exemple, sans danger d'interruption. Par exemple, pour l'équivalent de ++i : Interlocked.Increment(ref i); • Pour ce qui est des traitements généraux parallèles, la classe Parallel de .NET permet de faire facilement des boucles, de type for ou foreach, dont les itérations peuvent se produire en parallèle. • Il y a une version parallèle de LINQ, appelée PLINQ. Elle fournit un ForAll ! • Les fonctions de plusieurs classes sont, par défaut, thread safe, donc sans besoin de section critique ou exclusion mutuelle. C'est le cas de la classe Console par exemple. Pratique ! Plus technique, c'est le cas aussi de la classe Lazy par exemple, qui peut servir à la création thread safe des singletons. • Ces exemples nous montrent que Microsoft croit au parallélisme… !

  38. Petit programme complet C# class Intervalle { public Intervalle(intp_min, intp_max) { Minimum = p_min; Maximum = p_max; } public int Minimum { get; private set; } public int Maximum { get; private set; } } public class Program { public staticvoidClasseurPairImpair(objectp_intervalle) { Intervalle intervalle = (Intervalle)p_intervalle; for (int i = intervalle.Minimum; i <= intervalle.Maximum; ++i) { string message = (i % 2 == 0) ? " est " : " n'est pas "; Console.WriteLine(i + message + "un nombre pair."); } } public staticvoid Main() { Thread fil1 = new Thread(ClasseurPairImpair); fil1.Start(new Intervalle(100, 200)); // Si on n'avait pas besoin d'accéder au fil : // new Thread(ClasseurPairImpair).Start(new Intervalle(100, 200)); • Thread fil2 = new Thread(ClasseurPairImpair); • fil2.Start(new Intervalle(40, 45)); • fil2.Join(); // Attend sa fin • Console.WriteLine("On tue le fil 1 et on termine."); • fil1.Abort(); • } • } • Un résultat possible : • 100 est un nombre pair. • 101 n'est pas un nombre pair. • 102 est un nombre pair. • 103 n'est pas un nombre pair. • 104 est un nombre pair. • 105 n'est pas un nombre pair. • 106 est un nombre pair. • 40 est un nombre pair. • 41 n'est pas un nombre pair. • 42 est un nombre pair. • 43 n'est pas un nombre pair. • 44 est un nombre pair. • 45 n'est pas un nombre pair. • 107 n'est pas un nombre pair. • 108 est un nombre pair. • 109 n'est pas un nombre pair. • On tue le fil 1 et on termine. • 110 est un nombre pair.

  39. Autres considérations pour le parallélisme • Un des grands domaines de recherche actuels est sur les algorithmes (et collections) lock-free. En fait, si les sections critiques et l'exclusion mutuelle (qui bloquent les processus/fils donc les lock) pouvaient être évitées, la programmation concurrente pourrait évidemment être simplifiée. • Pour certaines classes de problèmes, c'est possible, et c'est ce que le terme lock-free veut dire dans ce contexte. Si on peut faire des classes pour des opérations classiques, en lock-free, les mettre en bibliothèque et populariser leur utilisation, la programmation concurrente sera plus simple, plus efficace et moins risquée. • Plus proche de nous, pour les données, les types immuables sont à privilégier. Dites adieu aux « setter » entre autres ! • Pour le code, il vaut mieux toujours penser à la réentrance. C'est souvent normal, mais il faut maintenant avoir de bonnes raisons pour s'en passer. • Les dangers que constituent les interblocages, actif ou non (deadlock et livelock), la famine (starvation) et l'inversion de priorité sont bien réels dans les systèmes qui utilisent beaucoup de parallélisme pour des raisons d'efficacité. Il est donc important d'en parler d'avance !

  40. Des ressources • D'après mes recherches, quelques livres de grandes qualités, à jour et pertinents : Campbell C. & al., ParallelProgrammingwith Microsoft .NET, Design Patterns for Decomposition and Coordination on Multicore Architecture, Microsoft 2010. Herlihy M., Shavit N., The Art of MultiprocessorProgramming, Morgan Kaufman, 2008. Duffy, Joe, Concurrent Programming on Windows, Addison Wesley, 2009.

More Related