Parallelisme (Basé sur Concepts of Programming Languages, 8th edition, by Robert W. Sebesta, 2007)
Les différents types de Parallelisme Le parallelisme dans l'exécution de logiciel peut se produire à quatre niveaux différents: Niveau des instructions de machines - exécutant deux ou plus instructions de machine simultanément. Niveau des instructions de code - exécutant deux instructions de code source ou plus simultanément. Niveau des Unités - exécutant deux unités ou plus de sous-programme simultanément. Niveau des Programmes - exécutant deux programmes ou plus simultanément. Puisqu'aucune issue reliee a la conception de langue n'est impliquée dand le parallelisme au niveau des instructions-machine et au niveau des programmes, ils ne sont pas discutés dans ce cours.
Les différents types d‘architectures a processeurs multiples Les deux catégories les plus communes d’architectures a processeurs multiples sont: Single-Instruction Multiple-Data (SIMD) —Architectures a processeurs multiples qui exécutent la même instruction simultanément, chacun sur des données différentes. Multiple-Instruction Multiple-Data (MIMD) — Architectures a processeurs multiples qui opèrent indépendamment mais dont les opérations peuvent être synchronisés.
Les différentes catégories de Parallelisme Il y a deux catégories distinctes de control parallele d'unités : Le parallelisme Physique— Plusieures unités appartenant au même programme sont exécutées littéralement en parallele sur différents processeurs. Le parallelisme Logique - Plusieures unités appartenant au même programme semblent (au programmeur et a l'application) etre exécute en parallele sur différents processeurs. En fait, l'exécution réelle des programmes a lieu de maniere intercalée sur un processeur simple. Pour le programmeur et le createur de langage, les deux types de parallelisme sont les mêmes.
Tâches I Une Tâche ou un processus est une unité de programme, semblable à un sous-programme qui peut être exécutée en parallele avec d'autres unités du même programme. Il y a trois différences entre les tâches et les sous-programmes: Une tâche peut commencer implicitement tandis qu'un sous-programme doit être appelé explicitement. Quand une unité de programme appelle une tâche, elle n'a pas besoin d'attendre que la tâche à accomplir soit terminée avant de continuer la sienne. Quand l'exécution d'une tâche est accomplie, la commande peut ou peut ne pas retourner à l'unité qui l'a appelée.
Tâches II Il y a deux catégories de Tâches: Heavyweight Tâches — tâches exécutées dans leur propre espace mémoire. Lightweight Tâches — tâches qui fonctionnent toutes dans le même espace mémoire. Lightweight tâches sont plus faciles a mettre en application que les heavyweight tâches. Les tâches peuvent typiquement communiquer avec d’autre tâches afin de partager le travail nécessaire pour accomplir le programme . Les tâches qui ne communiquent pas avec ou n'affectent pas l'exécution d'autres tâches s’appellent des tâches disjointes. Typiquement, les tâches ne sont pas disjointes et doivent synchroniser leur exécution, partager les données, ou les deux a la fois.
Synchronisation La synchronisation est un mécanisme qui controle l'ordre dans lequel les tâches sont exécutées. Ceci peut être fait par la coopération ou la compétition. La synchronisation pour coopération est exigée entre une tâche A et une tâche B quand la tâche A doit attendre que la tâche B soit terminée pour continuer son exécution. La synchronisation pour competition est exigée entre deux tâches quand toutes deux exigent l'utilisation d'une certaine ressource qui ne peut pas être employée simultanément. Pour la synchronisation pour coopération, des tâches spécifiques doivent être accomplies avant qu’une nouvelle tâche puisse-t-etre exécuté, tandis que, pour la synchronisation pour competition, certaines ressources doivent etre liberees avant qu’une nouvelle tâche s'exécute.
Un exemple de synchronisation pour coopération Le Problème Du Producteur et du Consommateur mémoire tampon Programme 1 (Producteur) Programme 2 (Consommateur) Le programme 1 produit des données ; Le programme 2 emploie les données. La synchronisation est nécessaire: L'unité du consommateur ne doit pas prendre des données si la mémoire tampon est vide L'unité du producteur ne peut pas placer de nouvelles données dans la mémoire tampon si elle n'est pas vide
Exemple de synchronisation pour competition I Nous avons deux tâches (A et B) et une variable partagée (TOTAL) La tâche A doit additionner 1 à TOTAL La tâche B doit multiplier TOTAL par 2. Chaque tâche accomplit son opération en utilisant le processus suivant: Chercher la valeur dans TOTAL effectuer l'opération arithmétique Remettre la nouvelle valeur dans TOTAL TOTAL a une valeur originale de 3.
Exemple de synchronisation pour competition II Sans synchronisation pour competition, 4 valeures peuvent résulter de l'exécution des deux tâches: Si A s’accomplit avant que B ne commence 8 Si A et B cherchent le TOTAL avant que l'un ou l'autre remette la nouvelle valeur dedans, alors nous avons: Si A remet la nouvelle valeur dans TOTAL en premier 6 Si B remet la nouvelle valeur dans TOTAL en premier 4 Si B s’accomplit avant qu'A ne commence 7 Ce genre de situation s'appelle un état de course parce que deux taches ou plus font la course pour utiliser les ressources partagées et le résultat dépend de quelle tache arrive la premiere.
Comment pouvons nous permettre l’acces mutuellement exclusif à une ressource partagée? I Une méthode générale consiste à considérer la ressource en tant qu’entite qu’une tâche peut posséder et ne permettre qu’a une tâche de la posseder à la fois. Afin de posseder une ressource partagée, une tâche doit demander la permission d‘y accéder. Quand une tâche a fini d’utiliser une ressource partagée qu'elle possède, elle doit l’abandonner de maniere a ce que la ressource soit rendue disponible à d'autres tâches.
Comment pouvons nous permettre l’acces mutuellement exclusif à une ressource partagée? II Afin que cet arrangement général marche, nous devons poser deux conditions: Il doit y avoir une manière de retarder l'exécution des tâches L'exécution de tâches doit être controllée L'exécution de tâches est controllée par le scheduleur qui contrôle le partage des processeurs parmi les tâches en créant des tranches de temps et les distribuant tour a tour. Le travail du scheduleur, cependant, n'est pas aussi simple qu'il peut sembler en raison des delais de taches qui sont nécessaires pour la synchronisation et l’attente pendant les opérations d'entrée-sortie.
États des tâche Afin de simplifier l‘implantation d’attentes pour la synchronisation, les tâches peuvent être dans différents états: Nouvelle : la tâche a été créée mais n'a pas encore commencé son exécution Prête : la tâche est prête à executer mais elle n’execute pas en ce moment. Elle se trouve dans la file d'attente des tâches prêtes . En execution : la tâche est exécutée en ce moment Bloquée : la tâche n’est pas presentement en execution parce qu'elle a été interrompue par un de plusieurs événements (habituellement une opération d'I/O). Morte : Une tâche meurt une fois son exécution terminee ou lorsqu’elle est explicitement tuée par le programme.
Perte de Vie Supposez que les tâches A et B aient besoin des ressources X et Y pour terminer leurs travaux. Supposez que la tâche A gagne la possession de X et la tâche B gagne la possession de Y. Après une certaine exécution, la tâche A a besoin de gagner possession de Y, mais doit attendre que la tâche B la libère. De même la tâche B doit gagner possession de X mais doit attendre que la tâche A la libere. Ni l'une ni l'autre des tâches n'abandonne la ressource qu‘elle possède, et en conséquence, toutes les deux perdent leur vie. Ce genre de perte de vie s'appelle un inter blocage ou “deadlock”. Les inter blocages sont des menaces sérieuses à la fiabilité d'un programme et doivent être évitées.
Question de conception pour le parallelisme Mécanismes pour la synchronisation Nous discutons maintenant de trois méthodes qui permettent l’acces mutuellement exclusif aux ressources: Les Sémaphores Les Moniteurs Le passage de Messages Dans chaque cas, nous discuterons de la maniere dont la méthode peut être employée pour implanter la synchronisation pour coopération et la synchronisation pour competition.
Sémaphores I Une sémaphore est une structure de données se composant d'un nombre entier et d'une file d'attente qui stocke des descripteurs de tâche. Un descripteur de tâche est une structure de données qui stocke toutes les informations appropriées sur l'état d'exécution d'une tâche. Le concept d'une sémaphore est que, pour fournir l'accès limité à une structure de données, des gardes sont placées autour du code qui accède à la structure.
Sémaphores II Un garde permet au code gardé d'être exécuté seulement quand une condition particuliere est vraie. Un garde peut être employé pour permettre à une seule tâche à la fois d'accéder à une structure de données partagée. Une sémaphore est l’implantation d'un garde. Les demandes d'accès à la structure de données qui ne peut pas être honorées sont stockées dans la file d'attente du descripteur de tâche de la sémaphore jusqu'à ce que l'accès puisse-t-etre accorde. Il y a deux opérations liées à une sémaphore : attendre et libérer
Sémaphores : Opérations d‘Attente et de Libération Release(Sem) If Sem’s queue is empty (no task is waiting) then Increment Sem’s counter else Put the calling task in the task-ready queue Transfer control to a task from Sem’s queue. Wait(Sem) If Sem’s counter > 0 then Decrement Sem’s counter else Put the caller in Sem’s queue Attempt to transfer control to some ready task (if the task queue is empty, deadlocks occur)
Synchronisation De Coopération: Le Problème Du Producteur Consommateur défini à l'aide des sémaphores semaphore fullspots, emptyspots; fullspot.count = 0 Emptyspot.count = BUFLEN task producer loop -- produce VALUE -- wait(emptyspots); DEPOSIT(VALUE); release(fullspots); end loop end producer task consummer loop wait(fullspots); FETCH(VALUE); release(emptyspots); -- consume VALUE -- end loop end consumer
Synchronization de Competition Exécution partagée de la mémoire tampon implementation avec semaphores semaphore access, fullspots, emptyspots; Access.count = 1; fullspot.count = 0; Emptyspot.count = BUFLEN; task producer loop -- produce VALUE -- wait(emptyspots); wait(access); DEPOSIT(VALUE); release(access); release(fullspots); end loop end producer task consummer loop wait(fullspots); wait(access); FETCH(VALUE); release(access); release(emptyspots); -- consume VALUE -- end loop end consumer
Inconvénients des sémaphores L'utilisation des sémaphores pour la synchronisation crée un environnement peu sûr. Dans La Synchronisation De Coopération: L’oubli de l'instruction wait(emptyspots) causerait un débordement de la mémoire tampon. L’oubli de l'instruction wait(fullspots) causerait un underflow de la mémoire tampon Dans La Synchronisation De Competition : L’oubli de l'instruction wait(access) de l'une ou l'autre des tâches peut causer un accès peu sûr à la mémoire tampon L’oubli de l'instruction release(access) de l'une ou l'autre des tâche peut causer un interblocage. Aucune de ces erreurs ne peut être vérifiée au moment de la compilation puisqu'elles dépendent de la sémantique du programme.
Moniteurs Les moniteurs résolvent les problèmes des sémaphores en encapsulant les structures de données partagées avec leurs opérations et en cachant leur exécution. Monitor Processus Sub 1 Mémoire Tampon Insérer Processus Sub 2 Supprimer Processus Sub 3 Processus Sub 4
Synchronisation de competition et de coopération en utilisant des moniteurs Synchronisation De Competition : Puisque tous les accès sont résidents au le moniteur, l‘implantation du moniteur peut garantir l'accès synchronisé en permettant seulement un accès à la fois. Synchronisation De Coopération : La coopération entre les processus demeure a la charge du programmeur qui doit s'assurer qu'une memoire tampon partagée ne subisse pas d’”underflow” ou de débordement. Évaluation : Les moniteurs sont une meilleure manière de fournir la synchronisation que les sémaphores, bien que certains des problèmes des sémaphores dans l'exécution de la synchronisation de coopération se retrouvent.
Passage de Messages Synchronisé Supposez que la tâche A et la tâche B sont toutes deux en exécution, et que A souhaite envoyer un message à B. Si B est occupé, il n'est pas souhaitable de permettre à une autre tâche de l'interrompre. Au lieu de cela, B peut signaler à d'autres tâche le moment ou il est prêt a recevoir des messages. A ce moment la, la tâche A peut envoyer un message. Quand la transmission a enfin lieu, nous parlons d’un rendez-vous. Le passage de messages (synchronise ou nom) est disponible en Ada. La Synchronisation de competition et la Synchronisation de coopération peuvent être toutes deux mises en application en utilisant ce paradigme.
Le Parallelisme en Java: Threads Les unités concourantes en Java sont des méthodes appelées run dont le code peut être exécute en parallele avec d'autres méthodes du meme type (appartenant a d'autres objets) et avec la méthode principale. Le processus dans lequel la méthode run est exécutee s'appelle un thread. Les threads du Java sont des tâches lightweight, ce qui signifie qu’elles sont toutes executees dans le même espace memoire. Pour définir une classe contenant une méthode de type run, on peut définir une sous-classe de la classe prédéfinie thread et remplacer sa methode run par une nouvelle methode.
Le Classe Thread Dans la classe Thread, il y a deux méthodes predefinies run et start. Le code de la méthode run décrit les actions de Thread. La méthode start commence sa thread comme unité concourante en appelant sa méthode run. Quand un programme a des threads multiples, un scheduleur doit déterminer quels threads s’executeront à quel moment. La classe Thread fournit plusieures méthode pour le control de l'exécution des threads: yield : demande a un thread en exécution de rendre le processeur sleep : bloque un thread pendant un nombre specifié de millisecondes join : force une méthode a retarder son exécution jusqu'à ce qu'une autre thread ait accompli son exécution interrupt : envoie un message à un thread, le forcant a terminer.
Priorité des Threads Les threads peuvent avoir différentes priorités. La priorité de défaut d'une thread est la meme que celle de la thread qui l'a créée. La priorité d'une thread peut être changée en utilisant la méthode setPriority. getPriority retourne la priorité actuelle d'une thread. Quand il y a des threads a différentes priorités, le comportement du scheduleur est commandé par ces priorités. Une thread a priorité inférieure sera executee seulement s’il n’y a pas de thread a priorité plus élevée dans la file d'attente quand une occasion se présente.
Synchronisation de competition en Java I En Java, la synchronisation de competition est implantee en indiquant qu’une méthode ayant accès à des données partagées doit avoir finie son execution avant qu'une autre méthode soit exécutée sur le même objet. Ceci est fait en ajoutant le modificateur synchronized à la définition de la méthode. Class ManageBuf{ Private int [100] buf; … Public synchronized void deposit (int item) {… } Public synchronized void fetch (int item) {… } } Un objet dont les méthodes sont toutes synchronisées correspond a un moniteur.
Synchronisation de competition en Java II Un objet peut avoir plus d’une méthode synchronisée; il peut egalement avoir une ou plusieures méthodes non synchronisees. Si pour une méthode particulière, seul une petite partie des instructions emploient la structure de données partagée, on peut employer une instruction synchronisée seulement pour la partie du code qui emploie la structure de données partagée : Synchronize (expression) instructions(s) Remarque: l'expression évaluee correspond à un objet Les objets a méthodes synchronisées doivent avoir une file d'attente liée à eux, pour stocker les méthodes synchronisées qui ont essayé de s’executer sur eux.
Synchronisation de Coopération en Java La synchronisation de coopération en Java emploie trois méthodes définies dans Object, la classe souche du Java. Ce sont les methodes suivantes: wait(): chaque objet a une liste d'attente contenant toutes les threads qui ont appelé wait() sur l'objet. notify(): est employé pour dire a un thread en attente que l'événement qu'il attendait s'est produit. notifyall(): réveille toutes les threads de la liste d'attente de l'objet, commençant leur exécution juste aprés leur appel a wait(). Notifyall est souvent employé a la place de notify. Ces trois méthodes peuvent seulement etre appelees de l’interieur d'une méthode synchronisée car elles utilisent la serrure placée sur un objet par une telle méthode
Un Exemple Java Voir le Manuel pp. 588-590
Évaluation du Parallelisme en Java Le parallelisme en Java est relativement simple mais efficace. Cependant, puisque les threads du Java sont “lightweight”, elles ne permettent pas aux tâches d'être distribuées sur des processeurs dotés de mémoires différentes, qui pourraient même se trouver sur des ordinateurs différents localisés a différents endroits . C'est la que l’implementation du parallelisme plus compliquée de l'ADA a des avantages par rapport à celle du Java.