Vers un nouveau paradigme de programmation parallèle en Java Mémoire présenté à la Faculté des études supérieures de l'université Laval pour l'obtention du grade de Maître ès Sciences (MSc.) Département d71nformat ique FXCULTÉ DES SCIENCES ET DE GÉNIE UNTVERSITÉ LAVAL Mai 1999 @ Mouetsie Molière? 1999
National Library Acquisitions and Bi bliographic Services Bibliothèque nationale du Canada Acquisitiins et services bibliographiques 395 Wellington Street 395. nre Wellmgton Ottawa ON KI A ONs OaawaON KlAW Canada canada The author has granted a nonexclusive licence aiiowing the National Library of Canada to reproduce, lom, distribute or sel1 copies of this thesis in microfom, paper or electronic formats. The author retains ownership of the copyright in this thesis. Neither the thesis nor substantial extracts fiom it may be printed or otherwise reproduced without the author's permission. L'auteur a accordé une licence non exclusive permettant à la Bibliothèque nationale du Canada de reproduire, prêter, distribuer ou vendre des copies de cette thèse sous la forme de microfiche/film, de reproduction sur papier ou sur format électronique. L'auteur conserve la propriété du droit d'auteur qui protège cette thèse. Ni la thèse ni des extraits substantiels de celle-ci ne doivent être imprimés ou autrement reproduits sans son autorisation.
Résumé Sous présentons dans ce mémoire un nouveau paradigme de programmation parallèle en Java. Ledit paradigme améliore la composante parallèle actuelle de Java en proposant des constructions s~taxiques qui abstraient la communication. la synchronisation et l'ordonnancement. Ce niveau d'abstraction est atteint en incorporant dans Java une algèbre de processus communiquants via des canaux. L'algèbre en question intègre des primitives de création dynamique de canaux et de processus. En outre, elle incorpore des combinateurs de séquence, de choix et de composition parallèle. Un trait à la fois éiégant et utile de cette algèbre est la possibilité de définir des calculs parallèles latents. Sous nous sommes inspirés du langage Concurrent ML (CML) pour l'élaboration de ladite algèbre. Yous proposons une évaluation de la composante parallèle de Java. Nous expliquons de manière détaillée tant au niveau conception qu'implantation le modèle de programmat ion parallèle en CML. Enfin, nous présentons l'architecture. l'implantation en Java ainsi que les bancs d'essais d'un système basé sur le paradigme sus-mentionné. Yourad Dgbabi Directeur recurche MouetWlière Étudiante
Remerciements Je tiens tout d'abord à remercier mon Directeur de recherche M. Mourad Debbabi. professeur au Département d'informatique de l'université Laval, qui grâce à ses conseils judicieu et son soutien, a rendu possible la réalisation de ce projet..je remercie également Mme Nadia Tawbi et M. Jean Bergeron, également professeurs au Département d'informatique de l'université Laval, qui ont bien voulu être les évaluateurs de cette thèse. Merci à M. Béchir Ktari pour l'aide inestimable qu'il m'a apportée pour la compréhension de l'implantation de CML. un remerciement spécial va à mes parents pour leur soutien continu pendant ces longues années d'études. Merci également à tous les membres l'équipe LSFM avec qui les discussions ont toujours apport6 sinon solutions, bonne humeur. Enfin, je remercie toutes les personnes qui de près ou de loin ont contribué à la réalisation de ce projet.
2.6.3 État Non-exécutable... 28 2.6.4 État mort... 28 2.7 Sotions supplémentaires... 28 2.7.1 Threads Démon... 38 2.7.2 Groupes de threads... 29 2.8 Conclusion... 30 3 Programmation parallele en CML 31 3.1 Introduction... 31 3.2 Primitives de parallélisme... 32 3-21 Threads... 32 3.2.2 Canaux... 34 3. 2.3 Exemple d'utilisation de t hreads et de canaux... 34 3.2.4 Événements... 33 3.2-5 Événements simples... 36 3.2.6 Communication sélective... 36 3.3 Implantation... 3'7 3.3.1 Continuations... 38 3.3.2 Implantation des Threads... 41 3.3.3 Implantation des canaux... 42 3.3.4 Implantation des événements... 44 3.1 Conclusion... 52 4 Étude comparative 54-1.1 Introduction... 34 4.2 Évaluation de Java... 54 4.2.1 Ordonnancement... 53 4.2.2 Portabilité... 55 4.2.3 Communication... 58 4.2.4 Synchronisation... 58 4.2.5 Sémantique formelle... 58 4.2.6.Abstractions... 59 4.2.7 Performance... 59 1.3 Évaluation de CML... 59 4.3.1 Communication... 60 4.3.2 Synchronisation... 60 4.3.3 Ordonnancement... 61 4.3.4 Abstraction... 61 3 Portabilité... 61 4.3.6 Sémantique formelle... 61 4.3.7 Performance... 62 4.4 Conclusion... 62
Liste des tableaux 2.1 Classe Reentrant... 17 2.2 Classe Auteur... 19 2.3 Classe Lecteur... 20 2.4 Classe Bibliothèque... 21 2. Classe AuteurLecteur: Programme principal... 21 2.6 Cas de famine... 24 2.7 Utilisation de yield... 26 3.1 Signature CML... 33 3.2 Opérations sur les threads et Leurs identificateurs... 34 3.3 Opérations sur les canaux... 35 3.4 Exemple d'utilisation de threads et de canaux... 36 3.5 Exemple d'utilisation des événements et de la sélection... 37 3.6 LtiIisationdeCallcc... 39 3.7 Utilisation de throw... 39 3.8 Fonction spawn... 42 3.9 Fonction send... 44 3.10 Fonction recv... 45 3.11 Structure des événements... 46 3.12 Fonction sendevt... 47 3.13 Fonction recvevt... 49 3.14 Fonction choose... 50 3.15 Fonction wrap... 51 3.16 Fonction syncononeevt... 32 3.11 FonctionsyncOnBEvt... 53-4.1 Exécution de threads à priorité égale... 56 5.1 Codage du Producteur/Consommateur en HOJ... 79 52 Équations des Philosophes... 80 5.3 Équations de l'ordonnanceur... 82 5.4 Résultats... 82
Chapitre 1 Introduction 1.1 Motivation et problématique Le langage Java est doté de nombreuses qualités très appréciées dans le monde de la programmation actuelle. C'est un langage orienté-objet simple qui est a la fois. neutre d'architecture, parallèle et sécurisé. Certains problèmes ont toutefois été relevés au niveau du volet parallèle de Java plus particulièrement en ce qui concerne les aspects de communication, de synchronisation et d'ordonnancement des threads. La programmation parallèle en Java est basée sur le concept de processus légers ou threads. Les threads Java communiquent et se synchronisent entre eu via des variables partagées dont l'intégrité est gérée par des moniteurs au niveau machine et par les primitives wait. notih et notifyall au niveau programmation. La gestion des moniteurs est souvent compliquée et peut mener à des résultats erronés lorsqu'elle n'est pas effectuée de manière adéquate. De plus, le langage Java manque d'abstractions et ne bénéficie pas réellement d'une définition formelle. Ce projet a pour but d'améliorer la compo, Gante parallèle actuelle de Java en proposant des constructions syntaxiques qui abstraient la communication, la synchronisation et l'ordonnancement. Ce niveau d'abstraction est atteint en incorporant dans Java une algèbre de processus communicants via des canaus. L'algèbre en question intègre des primitives de création dynamique de canaux et de processus ainsi que des combinateurs de séquence, de chois et de composition parallèle. 1.2 Langages parallèles -Avant de considérer la programmation parallèle au niveau d'un langage de programmation particulier, il est important de définir les mécanismes de fonctionnement des langages parallèles en général. II existe deux grandes catégories de langages de programmation parallèle: 0 tes langages utilisant la mémoire partagée; O Les langages utilisant la mémoire distribuée;
1.3 Langages a mémoire partagée 9 1.3 Langages à mémoire partagée Les langages basés sur la mémoire partagée utilisent des variables partagées pour l'implantation de la communication des processus. Ces langages se distinguent par les mécanismes qu'ils utilisent pour la synchronisation et le contrôle du parallélisme [Rep92a]. Parmi les mécanismes de synchronisation, nous distinguons les mécanismes de synchronisation de bas niveau tels que les sémaphores et les mécanismes de haut niveau tels que les moniteurs. 1.3.1 Sémaphores Les sémaphores proposés par Dijkstra [DijE] constituent le mécanisme de synchronisation de base. Un sémaphore est constitué d'une variable entière e(s) et d'une file d'attente f(s) [CRO751. En dehors de leur valeur initiale qui est positive ou nulle! les variables e(s) peuvent prendre des valeurs positives, négatives ou nulles sur lesquelles peuvent être appliquées deux opérations de base: P ( du Hollandais Passeren) pour l'attente et V ( du Hollandais Vrygeven) pour la notification. Lorsqu'un processus exécute P(s) où s est un sémaphore, sa variable e(s) est décrémentée. Si le résuitat de cette décrémentation est strictement négatif, alors le processus se bloque et se met dans la file d'attente f(s). L'exécution de V(s) est nécessaire pour débloquer le processus. Cette opération à pour effet d'incrémenter la variable e(s). Si le résultat est négatif ou nul alors un processus est récupéré de la file d'attente et son état redevient actif. Les verrous d'exclusion mutuelle ou mutex locks encore appelés sémaphores binaires sont une forme restreinte de sémaphores. Ils ont deux états: verrouille ou déverrouillé. Les verrous assurent l'exclusion mutuelle dans les régions critiques (partagées simultanément par plusieurs processus)mais ne fournissent pas de mécanisme général de synchronisation. Le principal inconvénient des sémaphores est qu'il n'existe pas de syntaxe fixe menant à une utilisation correcte. De plus, l'implantation de patrons plus compliqués que I?esclusion mutuelle se révèle particulièrement ardue. 1.3.2 Moniteurs Les travaux parallèles de Brinch Hansen et Hoare [Hoa741 ont conduit à un mécanisme assurant l'exclusion mutuelle basé sur des moniteurs. Les moniteurs sont des modules qui permettent l'abstraction des données et l'exclusion mutuelle. Tout code déclaré à l'interne ne peut être exécuté que par un processus à la fois. Les autres processus sont mis en attente jusqu'à la libération du moniteur.
1.4 Langages à mémoire distribuée 10 Un moniteur possède deux variables principales de synchronisation ou variables de conditions: wait qui bloque l'exécution du processus appelant; signal qui débloque un processus bloqué par un wait; Les moniteurs encapsulent les variables partagées. Ils fournissent une forme plus structurée d'exclusion mutuelle que ies verrous. En fait, chaque moniteur possède implicitement son propre verrou. Ce verrou est activé à l'entrée d'une procédure possédant un moniteur et il est désactivé a sa sortie. 1.4 Langages à mémoire distribuée La communication au niveau des langages utilisant la mémoire distribuée se fait par passage de messages. Le passage de messages consiste en un transfert de données synchronisé entre deux processus. C'est à dire que pour transférer un message, la coopération de l'envoyeur et du receveur est requise. Tout langage communiquant par passage de message possède deux opérations de base: l'envoi et la réception. Il est caractérisé par les propriétés spécifiques des médiums et des protocoles de communication entre les processus [ASSOI. Deux types de protocoles sont couramment utilisés: les protocoles asynchrones: envoi non-bloquant de messages: les protocoles synchrones: envoi bloquant de messages; Sous pouvons également mentionner les protocoles de requête-réponse ou "remote procedure calls". Les communications utilisant ce protocole s'effectuent par des appels et des retours de procédures. 1.4.1 Passage de message asynchrone Dans le passage de messages asynchrone le médium de communication est muni d'une variable tampon qui stocke les messages et l'opération d'envoi est non bloquante. Le Processus envoyeur continue son exécution indépendamment de Ia réception du message. La figure 1.1 représente la communication asynchrone entre dewc processus P et Q. Notons que ces deux processus ont une tue différente de l'ordre des événements. Le processus Q peut envoyer un message avant de recevoir un message de P même si ce dernier à été envoyé le premier.
1. -4 Langages à mémoire distribuée 11 Figure 1.1: Passage de message asynchrone. i 1.4.2 Passage de message synchrone Dans le passage de messages synchrone' l'opération d'envoi est bloquante. Le processus envoyeur ne peut continuer son exécution que si son message a été reçu par un autre processus. La figure 1.2 utilise la notation CSP pour l'envoi et la réception de messages entre les processus P et Q. Soit Q! v pour l'envoi de la valeur v par le processus Q et PI x pour la réception d'une valeur x par le processus P. Lorsque le processus P envoie la valeur v au processus Q, il reste en attente jusqu'à la réception de v par Q et vice versa. Les traits discontinus sur la figure illustrent les temps d'attente des processus. (a) Paltend Q (b) Q ancnd P Figure 1.2: Passage de message synchrone.
1.5 Objectifs et contributions 12 1.5 Objectifs et contributions L'objectif principal de ce projet est de proposer un nouveau paradigme de programmation dans le but de faciliter la programmation des threads en Java. Ce paradigme améliore la composante parallèle actuelle de Java en proposant des constructions SMtaviques qui abstraient la communication, la synchronisation et l'ordonnancement. Ce niveau d'abstraction est atteint en incorporant dans Java une algèbre de processus communiquant via des canaux. L'algèbre en question intègre des primitives de création de canaux et de processus ainsi que des combinateurs de séquence. de choix et de composition parallèle. Après une évaluation approfondie des composantes parallèles des langages Java et Concurrent ML. nous expliquons de manière détaillée la conception et l'implantation d'un nouveau paquetage de threads incorporant des concepts qui: définissent des abstractions de communication et de synchronisation. augmentent l'expressivité du langage en permettant de définir des communicat ions et des synchronisations latentes. 1.6 Structure Ce document est organisé en 6 chapitres. Mis à part le présent chapitre d'introduction, ie chapitre 2 décrit les principes de la programmation parallèle en Java. Le chapitre 3 explique les bases de la programmation en CML ainsi que son implantation. Une évaluation de ces deux langages est faite au chapitre 4, de même qu'une proposition de nouveau concepts à intégrer à Java. Le chapitre 5 explique le choix de conception, l'architecture et l'implantation d'un nouveau paquetage HOJ (Higher Order Java) pour le langage Java. Des programmes de tests sont inclus de manière à pouvoir évaluer son efficacité. Pour conclure, le chapitre 6 résume les principaux points détaillés tout au long de ce mémoire. Il expose les différents problèmes rencontrés pour la réalisation de ce projet ainsi que les solutions apportées.
Chapitre 2 Programmation parallèle en Java 2.1 Introduction au langage Java Le Iangage Java, connu initialement sous le nom de Oak, a été créé par Sun Microsystems en 1995 dans le but de répondre à des problèmes d'ordre pratique de la programmation moderne. La croissance importante de 1'Internet a en effet modifié les besoins au niveau du développement et de la distribution logicielle. Pour s'adapter à ces modifications. un langage de programmation doit permettre le développement d'applications sécurisées. performantes et hautement robustes sur des plates-formes multiples et dans des réseaux hétérogènes, distribués. Java a été conçu pour répondre aux besoins des développeurs travaillant dans des environnements réseaux hétérogènes. Il permet donc d'exécuter des applications indépendantes du système d'exploitation et de la machine utilisés dans un environnement sécurisé. Java est un langage auquel il peut être attribué un certain nombre de caractéristiques. C'est un langage à la fois: Orient6 objet: comme dans tous les langages de ce type, les données sont structurées en classes. Les classes contiennent des variables qui constituent la structure statique ainsi que des méthodes qui définissent le comportement dynamique des objets instances de ces classes. Ces dernières sont organisées selon une hiérarchie et héritent des propriétés de leurs super-classes. Le Iangage Java ne supporte pas l'héritage multiple donc une classe étant capable d'avoir plusieurs sous classes n'aura jamais qu'une seule super-classe. Simple: l'un des soucis majeurs lors de la conception du langage Java a été la sim plici té. Tout en héritant des caractéristiques de plusieurs autres langages tels C? C++ et SmallTalk, il se retrouve dénué de la pll~part des aspects qui augmentent leur complexité comme les pointeurs ou l'héritage multiple.
2.1 Introduction au iangage Java 14 Distribué: Java comporte une librairie permettant d'utiliser facilement des protocoles de communication TCP/IP tels que HTTP et FTP. De ce fait. une application Java peut accéder à des objets situés sur Internet via des URLs avec autant de facilité que des fichiers locaux. Parall&le: Java supporte la programmation parallèle c'est-à-dire que plusieurs processus ou threads sont en mesure de s'exécuter en parallèle avec la possibilité de s'échanger de l'information et de se synchroniser. Le langage Java intègre des primitives de synchronisation basées sur les paradigmes de variables de conditions et de moniteurs. Robuste: la robustesse de Java vient du fait qu'il est conçu pour être fiable dans de nombreuses situations. L'emphase est mise sur la vérification. Java effectue d'abord une vérification statique et recherche les problèmes éventuels. Une seconde vérification a lieu à l'exécution dans le but d'éliminer les situations génératrices d'erreurs. Sécurisé: Java à été créé afin de pouvoir, entre autres. être utilisé dans des environnements en réseau et distribués. De ce fait, la sécurité a constitué une contrainte importante lors de sa conception. Java procure deux niveaux de sécurité: l'un au niveau du langage et l'autre au niveau bytecode. Neutre d'architecture: les réseau en général sont composés de plusieurs systèmes avec des systèmes d'exploitation, des unités centrales et des architectures différents. En vue de permettre l'exécution des programmes Java sur des platesformes différentes et partout a travers le réseau, le compilateur génère du code objet pouvant être exécuté indépendamment de l'architecture: le bytecode. Interprété: Java est un langage interprété. Il est doté d'un interpréteur capable d'exécuter directement le bytecode sur n'importe quelle machine dotée d'une JVM. Le processus d'édition de liens n'est alors plus nécessaire et la rapidité du processus de développement s'en trouve accrue. Portable: la portabilité de Java est surtout due au fait qu'il est neutre d'architecture. De plus, sa spécification ne contient aucun aspect dépendant de l'implantation. La taille et les opération sur les t-vpes primitifs Java sont nettement spécifiées et les librairies définissent des interfaces portables.
2.2 Programmation parallèle 15 2.2 Programmation parallèle La programmation parallèle en Java est basée sur le concept de thread. Dans un système informatique, plusieurs programmes ou processus peuvent être fragmentés en plusieurs composantes. Ces composantes encore appelées processus légers ou threads sont des flots de contrôle ayant la possibilité d'exécuter des instructions en parallèle. Ainsi, un processus comprenant plusieurs threads est en mesure d'exécuter autant de tâches en parallèle qu'il comporte de threads. Les applications Java qui utilisent les threads permettent. mis à part l'exécution simultanée de plusieurs tâches. d'augmenter le degré d'interaction avec l'utilisateur. Elles permettent également d'implanter des comportements asynchrones [Ber96]. Bien que le paquetage de threads de Java n'offre pas toutes les possibilités que certains autres plus complets, il a l'avantage d'être simple d'utilisation. Cette simplicité rend la programmation des threads plus facile à comprendre et les applications, de ce fait, plus robustes. 2.3 Création de threads II existe deux manières de créer des threads en Java: 0 l'utilisation des sous-classes de la classe Thread: l'utilisation de l'interface Runnable: L'une des façons de créer un thread Java est de spécialiser la classe pré- définie Thread du paquetage java-lang et de redéfinir sa méthode run. Le thread ainsi créé ne s'esécute que lors de l'appel de la méthode start. Cette méthode une fois lancée appelle la méthode principale du thread. soit sa méthode run. Java ne supporte pas l'héritage multiple. Il est donc nécessaire de prévoir une méthode autre que la spécialisation pour la création de threads notamment dans le cas des applets où la spécialisation est également nécessaire. Dans ce cas, un objet qui implante l'interface prédéfinie Runna ble peut être créé. Cet objet fournira les méthodes nécessaires, en l'occurrence la met hode run? au nouveau t hread. 2.4 Synchronisation Tant que les threads s'exécutent sans interagir les uns avec les autres, le problème de synchronisation ne se pose pas. Cependant, dans la plupart des applications multithreads. ils travaillent avec des données partagées, échangent de l'information et par
2.4 Synchronisation 16 conséquent sont obligés de tenir compte de l'état et de l'activité des autres threads. Dans de telles situations, il devient impératif de synchroniser les processus légers. La synchronisation en Java se fait à l'aide de variables partagées. Les threads qui se synchronisent accèdent à tour de role à ces variables s ~it pour y lire soit pour y écrire des informations. La synchronisation est gérée à deux niveau: au niveau de la machine virtuelle par les moniteurs, au niveau de la programmation par les méthodes wait. notify et notisall. 2.4.1. Niveau machine virtuelle Au niveau de la machine virtuelle de Java, ta synchronisation des threads se fait grâce à l'utilisation de moniteurs. Lorsque demu threads se syuchronisent. ils se partagent des objets appelés variables de condition. Le rôle des moniteurs est d'empêcher deux threads d'accéder simultanément à une même variable de condition. Dans un programme, les sections de code pouvant être utilisées à la fois par deux ou plusieurs t hreads en parallèle sont dites critiques. Les sections critiques sont identifiées dans un programme Java par le mot clé synchronized. Les sections de code critique seront de préférence des méthodes pour éviter les problèmes lors du débogage. Un moniteur unique est associé à chaque objet ayant une méthode synchronized. Son acquisition et sa libération se font automatiquement par l'environnement d'exécution de Java. Les threads ont la possibilité de réacquérir un moniteur qu'ils ont déjà. C'est l'une des qualités des moniteurs désignée par le terme reentrant. Ceci empêche l'inter-blocage des threads sur un moniteur déjà acquis. Par exemple, considérons la classe Reentrant illustrée à la table 2.1. La classe Reentrant contient deux méthodes synchronisées: a et b. La première méthode fait appel à la seconde. Arrivé à la méthode a, le thread actuel prend le moniteur pour l'objet Reentrant. a appelle b qui est également étiquetée synchronized. Le thread doit donc ré-acquérir le moniteur. si le système ne supportait pas les moniteurs reentrant (pouvant être acquis plus d'une fois par le même thread), cette séquence aboutirait à un inter-blocage. 2.4.2 Niveau programmation La synchronisation du niveau programmation est obtenue en utilisant une combinaison des méthodes wait, notify et notifyall. Ces méthodes ne peuvent être invoquées qu'a
claa Reentrant.c public synchronized void a() C Systern.out.println("Je suis dans la méthode a()"); 1 public synchonized void b() 1 System.outprintln("Je suis dans la méthode b()"); } Tableau 2.1: Classe Reentrant. partir d'une méthode ou d'un bloc synchronized[cw981. Méthode wait La méthode wait sans arguments met le thread en état d'attente et ce jusqu'à ce qu'un autre thread l'avertisse d'un changement de condition grâce aux méthodes notify ou notisall. Ln thread peut attendre indéfiniment si aucune de ces méthodes n'est appelée. Deus autres versions de la méthode wait permettent la limitation de l'attente à un certain nombre de milisecondes ou même de nanosecondes: wait(long limite) wait(long limite, int nanos) Méthodes notify et notifyall La méthode notify a pour eeet d'avertir arbitrairement l'un des threads en attente de la disponibilité du moniteur. Tous les autres threads continuent d'attendre. Afin d'obtenir un résultat moins arbitraire, nous utiliserons la méthode notifyall qui au lieu d'avertir un thread unique, avertit tous les threads en attente sur le moniteur. Ces derniers sont alors en condition de course pour ré-acquérir le moniteur. 2.4.3 Exemple de synchronisation de t hreads Cet exemple est basé sur celui du Producteur/Consommateur illustré dans le manuel "The Java Tutorial" de SUN [CW981. Considérons un auteur! un lecteur et une bibliothèque. L'auteur écrit un livre avec un numéro de série pouvant aller de O à 9. Il
2.4 Synchronisation 18 dépose son livre dans une bibliothèque et imprime le numéro de son livre. Après quoi, il s'endort un certain temps avant d'écrire un nouveau livre. Le lecteur de son côté ne manque jamais une occasion de lire et emprunte tous les livres de la bibliothèque dès que ces derniers sont accessibles. L'auteur et le lecteur échangent des informations via la bibliothèque qui joue ici le rôle de variable partagée. Pour empêcher que le lecteur aille chercher un livre qu'il a déjà lu ou que l'auteur ne dépose de nouveau livre alors que le lecteur n'a pas lu le précédent, l'auteur et le lecteur doivent être synchronisés. Le lecteur ne doit lire qu'un livre à la fois. Le programme suivant utilise les deux mécanismes de synchronisation vu précédemment. soient les moniteurs et les méthodes wait, notify et notifyall. Les moniteurs empêchent l'auteur et le lecteur d'accéder simultanément à la bibliothèque. Les méthodes wait et notifiall sont utilisées au niveau de la bibliothèque pour assurer la coordination de l'auteur et du lecteur. Ces méthodes assurent que les livres déposés dans la bibliothèque ne sont empruntés qu'une seuie fois. Cet exemple permet d'illustrer les principaux aspects de la communication et de la synchronisation des threads Java. Le code donné par les tables 2.2 à 2.5 est composé de quatre classes principales: la classe Auteur qui définit le comportement du thread auteur. 0 la classe Lecteur qui définit le comportement du thread lecteur, la classe Bibliothèque qui constitue l'objet partagé par les deux threads? la classe AuteurLecteur qui constitue le programme principal. Classes Auteur et Lecteur Les classes Auteur et Lecteur sont définies comme étant des sous-classes de Thread. Elles héritent donc des propriétés et des méthodes de leur super-classe. Les classes Auteur et Lecteur contiennent deux données privées: une bibliothèque et un identificateur entier. Leur comportement est défini par la méthode run. Le producteur utilise la méthode déposer de la classe Bibliothèque pour envoyer une série d'entiers et le consommateur utilise la méthode emprunter de la classe Bibliothèque pour les récupérer. Classe Bibliothèque La classe Bibliothèque contient les méthodes nécessaires à la communication et à la synchronisation de l'auteur et du lecteur. Elle contient deux variables privées: un entier qui représente le contenu de la bibliothèque et un booléen enrayon indiquant
2.4 Synchronisation 19 class Auteur extends Thread { private Bibliothèque biblio: private int numéro; public auteur ( Bibliothèque b. int numéro) C biblio=b; this.numero=numero; 1 public void fun() { for (int i=o; i<10; i++) { biblio.deposer(i); system.out.println("l*auteur numéro " + this.numero+ " a déposé le manuel: If +i); try{sleep ((int) (Math.random() *100)); ) catch (InterruptedException e) {) 1 Tableau 2.2: Classe Auteur.
2.4 Synchronisation 20 class Lecteur extends Thread C private Bibliothèque biblio; private int numhro; pubiic lecteur ( Bibliothèque b, int numéro) C 3 public void run () C int manuel =O; for ( int i=0: i<10; i++) 1 manuel=biblio.emprunter(); system.out.println (II Le lecteur numéro +this.numero+ " a emprunte le manuel" +i); 1 1 Tableau 2.3: Classe Lecteur.
2.4 Synchronisation 21 class Bibliothèque { private int contenu; private boolean enrayon =faise; public synchronized int emprunter() { while (enrayon == false) try {wait(); ) catch (InterruptedException e){) 1 enrayon=false; notify All; return (contenu); 1 public synchronized void déposer ( int manuel) C while (enrayon == true) C try{wait(): ) catch ( InterruptedException e) {) 1 contenu =manuel; en Rayon= true; notifyall(); } Tableau 2.4: Classe Bibliothèque. class AuteurLecteur { public void main ( String fl args ) C Bibliothèque b = new Bibliothèque(); auteur a = new auteur (b.1); kteur I = new lecteur (b.1); a.start(); I.%art() ; > 3 Tableau 2.5: Classe AuteurLecteur: Programme principal.
la disponibilité de ce contenu. Elle contient également deux méthodes emprunter et déposer. Méthode emprunter La méthode emprunter est une méthode déclarée avec le modificateur synchronized, c'est à dire qu'un moniteur lui est associé pour empêcher son utilisation simultanée par plusieurs threads. Cette méthode est utilisée par le lecteur pour la récupération des données. Si la Bibliothèque est vide, alors le lecteur se met en attente jusqu7à ce qu'il soit notifié par l'auteur. La variable contenu est alors remplie. La variable enrayon est mise à false et le contenu est retourné. La méthode notisall est appelée afin d'avertir l'auteur. Méthode déposer Comme la méthode emprunter, Ia méthode déposer est une méthode déclarée avec le rnodificateu r synchronized. Elle est utilisée par l'auteur pour l'envoi de données. Si la bibliothèque est pleine alors l'auteur se met en attente jusqu'à ce qu'il soit notifié par le lecteur. Après quoi, la valeur à envoyer est mise dans Ia variable contenu. La enrayon est mise à true et le lecteur est averti. Exécution L 'esécut ion du programme génère les lignes suivantes: L'auteur numéro 1 a dépose le manuel: O Le lecteur numéro 1 a emprunté le manuel: O L'auteur numéro 1 a déposé le manuel: 1 Le lecteur numéro 1 a emprunté le manuel: 1 L'auteur numéro 1 a déposé le manuel: 2 Le lecteur numéro 1 a emprunté le manuel: 2 L'auteur numéro 1 a déposé le manuel: 3 Le lecteur numéro 1 a emprunté le manuel: 3 L'auteur numéro 1 a déposé le manuel: 4 Le lecteur numéro 1 a emprunte le manuel: 4 L'auteur numéro 1 a déposé le manuel: 5 Le lecteur numéro 1 a emprunté le manuel: 5 L'auteur numéro 1 a déposé le manuel: 6 Le lecteur numéro 1 a emprunté le manuel: 6 L'auteur numéro 1 a déposé le manuel: 7 Le lecteur numéro 1 a emprunté le manuel: 7 L'auteur numéro 1 a déposé le manuel: 8 Le lecteur numéro 1 a emprunté le manuel: 8 L'auteur numéro 1 a déposé le manuel: 9 Le lecteur numéro 1 a emprunté le manuel: 9
2.5 Ordonnancement 23 2.5 Ordonnancement La programmation parallèle en Java est relativement simple mais nécessite la maîtrise de quelques concepts d'ordonnancement de manière à éviter certains comportements imprévus. L'ordonnancement est le processus qui gère l'exécution d'un ensemble de taches sur un processeur. En Java, il s'agit du processus chargé du choix des threads et de leur exécution à un moment donné au niveau de la machine virtuelle. 2.5.1 Priorité Dire que les threads s'exécutent de façon simultanée n'est pas tout à fait véridique. Puisqu 'ils doivent souvent se partager un processeur unique, ils s'exécutent les uns après Ies autres mais de façon à créer une illusion de parallélisme. L'environnement d'exécution de Java supporte un algorithme simple et déterministe d'ordonnancement : l'ordonnancement à priorité fixe. Cet algorithme définit l'ordre d'exécution des threads en se basant sur leur priorité par rapport aux autres. X sa création? un thread Java hérite automatiquement de la priorité de son thread créateur. Cette priorité peut varier entre 1 (priorité minimum) à 10 (priorité maximum). Il est toutefois possible de modifier cette priorité après la création du thread en utilisant la méthode setpriority. Les lignes de code suivantes illustrent la création d'un t hread tl et le changement de sa priorité en utilisant cette méthode: Thread tl= new Thread(); tl.setpriority(3); // donne la priorité 3 a tl tl.nart(): Lorsque plusieurs threads sont prêts à s'exécuter, l'environnement d'esécution choisit le thread de priorité la plus élevée. Un autre thread de priorité plus faible ne pourra s'exécuter que si le premier est interrompu provisoirement ou définitivement. Lorsque deux threads de priorité égale attendent que le processeur soit disponible, I'ordonnanceur choisit l'un d'entre eux suivant l'algorithme de round-robin. Le thread choisi s'exécutera jusqu'à ce que l'une de ces conditions soit vraie: Ln thread de priorité plus élevée devient exécutable (préemption), Le thread en question s'arrête provisoirement ou définitivement, Le système supporte le time-slicing et son temps a expiré. 2.5.2 Accès à l'unité centrale Lorsque plusieurs threads sont en compétition pour l'accès a u ressources, il faut s'assurer que justice soit faite.
2.5 Ordonnancement 24 public class Nonjuste extends Thread public Nonjuste( String nom) C super(nom) ; public void run() { for (int=o; i<5; i++) System.out.println(getName()); 1 public static void main (String argsu) C new Nonj~ste("threadl'~).stan(): new Nonju~te(~'thread2").sa rt(): new Nonjuste("thread3").nart(); Tableau 2.6: Cas de famine. Ln système est dit juste (fair) si chaque thread a un accès suffisant aitu ressources du système de manière à pouvoir évoluer. Si le système est juste: les problèmes de "famine" (starvation) et d'inter-blocage sont éliminés. Il y a famine lorsqu'un ou plusieurs threads ne peuvent pius évoluer faute d'accès aux ressources. La forme ultime de famine est le deadlock. Il y a deadlock lorsque deu-x ou plusieurs threads sont en attente sur une condition qui ne peut être satisfaite. Le plus souvent. le deadlock est causé par deux ou plusieurs threads s'attendant mutuellement pour agir. On dit d'un thread qu'il est non juste s'il ne redonne jamais volontairement le contrôle à I'unité centrale. Il s'exécute sans interruption jusqu'à la fin de sa méthode run ou jusqu'à ce qu'un thread de priorité supérieure s'exécute (préemption). Dans l'exemple 2.6. les threads non justes s'exécutent jusqu'à la fin sans tenir compte des autres. Le résultat d'exécution de l'exemple 2.6 est le suivant:
2.5 Ordonnancement 25 threadl threadl threadl threadl threadi t hread2 thread2 thread2 thread2 thread2 thread3 t hread3 thread3 thread3 thread3 La programmation de ces threads peut bloquer l'accès à l'unité centrale pour un temps plus ou moins long et de ce fait. modifier les résultats escomptés. Les méthodes sleep et yield peuvent être utilisées pour défaire les threads de ce défaut. La méthode sleep(n bmilisecondes) a pour effet d'arrêter l'exécution du t hread pour un certain nombre de milisecondes. Avec la méthode yield, un thread peut "volontairement" libérer l'unité centrale pour permettre à d'autres threads ayant la même priorité de s'exécuter. Cette méthode est sans effet si aucun thread de priorité égale n'est en attente pour s'exécuter. La table 2.7 montre l'utilisation de La méthode yieldpour prévenir le comportement non juste des threads. Le résultat de l'exécution est: threadl thread2 thread3 threadl thread2 thread3 threadl thread2 thread3 threadl thread2 thread3 threadl t hread2 t hreact3
2.5 Ordonnancement 26 public class juste extends Thread C public juste( String nom) super(nom) ; 1 public void run() C for (int=o; i<5; i++) yield() ; 1 public static void main (String argso) C "ew juste("threadil').start(): new juste("thread2").start(); new juste("thread3").start(); } 1 Tableau 2.7: Utilisation de yield.
2.6 Différents états des threads Java 24 On obtient le même résultat en remplaçant la commande yield par: t v sleep(1000): 1 catch ( InterruptedException e) 2.6 Différents états des threads Java La figure 2.1 représente le cycle de vie ou l'ensemble des états possibles d'un thread Java. Figure 2.1: États des threads Java. 2.6.1 État Nouveau thread Ln thread à sa création se trouve a l'état né. C'est un objet pratiquement vide auquel aucune ressource système n'a encore été allouée. Lorsqu'un thread se retrouve dans cet état. il ne peut être exécuté qu'avec la méthode start. 2.6.2 État Exécutable Dès l'appel de la méthode start, le thread est dit exécutable ou prêt. Li faut cependant noter qu'un thread exécutable n'est pas pour autant en exécution. Il se trouve dans un
2.7?/O tions supplémentaires 28 état lui permettant d'être repéré par l'environnement d'exécution parmi les threads à exécuter- Le thread s'exécute véritablement lors de I'allocation du processeur. 2-6 -3 État Non-exécutable Lorsqu'un thread s'exécute, il peut être momentanément interrompu de diverses façons et se mettre dans l'un des états suivants: Prêt: lorsque la méthode yield est appelée. 0 En attente: lorsque la méthode wait est appelée..?\ partir de cet état le thread peut retourner à l'état prêt si la méthode notih ou notisall est appelée. 0 En hibernation: lorsque la méthode sleep est appelée. Le thread retourne à l'état prêt à la fin de son délai d'attente. O En suspens: lorsque la méthode suspend est appelée. L'exécution du thread peut reprendre a l'appel de la méthode resume. Bloqué: lors de la synchronisation sur des Entrées/Sorties. 2.6.4 État mort II existe d eu manière pour un thread de "mourir" et se retrouver dans l'état mort : soit sa méthode run se termine, soit l'on fait appel à la méthode stop. Il sera toutefois conseillé d'éviter l'appel de la méthode stop en plaçant dans ia méthode run, des drapeaux indiquant qu'elle doit arrêter son exécution. 2.7 Notions supplémentaires Le paquetage de threads de Java comprend également les notions de threads Daemon et de groupes de threads. Une description des ces deux notions est donnée dans les paragraphes qui suivent. 2.7.1 Threads Démon Cn thread Démon est un fournisseur de services pour les autres threads qui s'exécutent dans le même processus que lui. Prenons l'exemple du navigateur Hot Java. Pour ailer chercher des images contenues dans le système de fichiers. il utilise jusqu'à quatre t hreads Daemon appelés "Image F'etchers". T-ypiquement, la méthode run d'un t hread démon est une boucle infinie qui attend des demandes de services. Pour faire d'un thread un Démon, la méthode setdaemon avec l'argument true est utilisée. De la même manière, pour savoir si un thread est un Démon, il est possible d'utiliser la méthode isdaemon qui retourne les valeurs booléennes vrai ou faux.
3.7 Notions supplémentaires 29 2.7.2 Groupes de threads Ln groupe de threads a pour fonction principale de fournir des mécanismes visant à rassembler plusieurs threads dans un objet unique et à manipuler ces threads ensemble plutôt que de façon individuelle, sa création, un thread est placé dans un groupe par défaut appelé main. Ceperidant. si pour une raison l'on souhaite placer le thread dans un groupe particulier. il faut le spécifier dans le constructeur avant sa création. La classe Thread a trois constructeurs permettant de designer un nouveau groupe de thread comme le montre le code suivant: public Thread(groupe-de-thread groupe. Runna ble cible) public Thread(groupe-de-thread groupe. String nom) public Thread(groupe-de-thread groupe, Runnable cible. String nom) Les groupes de threads peuvent contenir à la fois des threads et d'autres groupes. La classe ThreadGroup contient des méthodes permettant : 0 d'organiser l'ensemble des threads ainsi que les sous-groupes contenus dans le groupe, de fiser ou de retrouver des attributs de l'objet ThreadGroup, de faire certaines opérations telles que démarrer ou suspendre tous les threads et sous-groupes du groupe, de permettre au gestionnaire de sécurité de fermer l'accès à certains threads selon leur groupe d'appartenance. Certaines méthodes n'agissent qu'au niveau du groupe en inspectant ou en changeant certains attributs de l'objet ThreadGroup et ce, sans modifier les threads du groupe. D'autres méthodes permettent de modifier le statut actuel de tous les threads à l'intérieur du groupe: suspend qui permet de suspendre momentanément un thread, stop qui permet de supendre définitivement un thread. O resume qui permet de réactiver un thread momentanément suspendu.
2.8 Conclusion Ce chapitre passe en revue les principales notions nécessaires a la programmation parallèle en Java. Ces notions vont de la création des threads à leur destruction en passant par leur communication et leur s~chronisation. Les threads Java sont créés par sousclassement de la classe prédéfinie java.lang.thread ou par implantation de l'interface Runnable. Ils communiquent et se synchronisent entre eux via des variables partagées dont la gestion est assurée par des moniteurs au niveau machine et par les méthodes wait, notify et notifyall au niveau de la programmation. Enfin, tous les threads appartiennent à un groupe et possèdent une priorité qui doit déterminer leur ordre d'exécution. Le chapitre suivant introduit le langage CML et décrit son fonctionnement ainsi que son implantation.
Chapitre 3 Programmation parallèle en CML 3.1 Introduction Le langage ML a été conçu par Robin Milner en 1978 et à été standardisé deus années plus tard en Standard ML (SML). C'est un langage de programmation fonctionnelle, ses fonctions sont des valeurs de première classe et son mécanisme de contrôle est principalement basé sur l'application de fonctions récursives. 11 existe actuellement plusieurs implantations de Standard ML dont: Standard bf1 NJ (Sew Jersey), 0 Poly ML, Edinburgh ML, Toutes ces implantations supportent un noyau syntaxique et sémantique commun mais diffèrent dans leurs extensions telles les messages d'erreurs. les exceptions ou les modules. Le langage Standard ML est interactif. fortement et implicitement typé, possède un système de types polymorphique et supporte les types abstraits ainsi que le traitement des exceptions. 11 est constitué d'un noyau ou langage de base pour la programmation à faible échelle ainsi que d'un système modulaire permettant la construction de programmes plus larges. Le langage Concurrent ML (CML) est une extension du langage ML standard (SML) conçue Par John Reppy en 1990 dans le but d'y ajouter des primitives de programmation parallèle ainsi que des opérations de première ciasse. Ce langage, ainsi que son précurseur (PML), intègre un mécanisme d'abstraction de haut niveau unique pour la programmation parallèle. Ce mécanisme est basé sur la séparation de l'opération de synchronisation en tant que telle des mécanismes décrivant les protocoles de synchronisation et de communication. En tant qu'extetision du ML standard (SML), CML hérite
3.2 Primitives de pardélisme 32 des principales caractéristiques de ce langage: O il est fortement typé, O il permet le polymorphisme, O il permet le filtrage, O il supporte la gestion d'exceptions, ses fonctions sont des valeurs de première classe. Les programmes CML sont composés d'un ensemble de processus légers ou threads. Ces threads, qui sont des évaluations d'expressions ML, peuvent communiquer entre eus en envoyant des messages via des canaux. La circulation des messages, qui se fait de manière s_vnchrone, constitue la base de la communication et de la s_vnchronisation en CML. 3.2 Primitives de parallélisme Les primitives de parallélisme sont données par la signature CML illustrée par la table 3.1. Elles permettent la création de processus ainsi que leur communication et leur synchronisation via des canaux Ces primitives peuvent être regroupées en trois catégories: Les threads. O Les canaux. O Les événements. 3.2.1 Threads Ln programme CiML est constitué d'un ensemble de threads s'exécutant en parallèle. Les threads sont créés de manière d~amiquen utilisant la commande spawn sur une fonction f. Cette commande qui évalue l'application de f à une valeur de type unit () de ML retourne un identificateur de thread thread-id pour le nouveau thread. Son implantation est détaillée à la section 3.3.2. Le nouveau thread est appelé le fils et son créateur, le parent. te thread fils s'exécute jusqu'à ce que l'évaluation de la fonction f soit terminée. Cependant il peut s'interrompre par l'utilisation de la commande exit ou par une exception non rattrapée. Plusieurs commandes peuvent être appliquées aux threads et à leurs identificateurs. Pour tester l'égalité de deux threads, la commande sametid peut être appliquée aux
3.2 Primitives de pardéfisme - - signature CML structure CML : CML type thread-id type 'a chan type 'a event val version : {synem : string, version-id : int list. date : string) val banner: string val spawnc: ('a -> unit) -> 'a -> thread-id val spawn : (unit -> unit) -> thread-id val yield: unit -> unit val exit : unit -> 'a val gettid : unit -> thread-id val sametid : (thread-id * thread-id) -> bool val comparetid : (thread-id * thread-id) -> order val hashfid : thread-id -> word val tidtostring: thread-id -> string val joinevt : thread-id -> unit event val channel : unit -> 'a chan val samechannel : ('a chan 'a chan) -> bool val send : ('a chan 'a) -> unit val rem : 'a chan -> 'a val sendevt : ('a chan * 'a) -> unit event val remevt: 'a chan -> 'a event val sendpoll : ('a chan * 'a) -> bool val recvpoll : 'a chan -> 'a option val wrap: ('a event * ('a -> 'b)) -> 'b event val wraphandler : ('a event (exn -> 'a event)) -> 'a event val guard : (unit -> 'a event) -> 'a event val withnack: (unit event -> 'a event) -> 'a event val choose : 'a event Iist -> 'a event val sync : 'a event -> 'a val select : 'a event list -> 'a val never : 'a event val alwaysevt : 'a -> 'a event val timeoutevt : Tirne-time -> unit event val attimeevt : Tirne-tirne -> unit event Tableau 3.1: Signature CML.
3.2 Primitives de parallélisme 34 type thread-id val spawn : (unit -> unit) -> thread-id val exit : unit -> 'a val samethread : (thread-id *thread-id) -> bool val tidlessthan : (thread-id*thread-id )-> bool val tidtostring : thread-id -> string val yield : unit ->unit Tableau 3.2: Opérations sur les t hreads et leurs identificateurs. identificateurs, Les identificateurs sont attribués dans un certain ordre afin d'éviter les dépendances cycliques pouvant mener à un inter-blocage. L'ordre de création des threads peut être testé avec la fonction tidlessthan. De même, une représentation des identificateurs sous forme de chaîne de caractères peut être obtenue avec la commande tidtostring. Un thread peut enfin céder la priorité en redonnant le contrôle à l'unité centrale en utilisant la commande yield. La table 3.2 illustre différentes opérations pouvant être exécutées sur un thread ou son identificateur. 3.2.2 Canaux Les threads CML communiquent entre eux via des canaux Ces canaux sont créés de manière dynamique en utilisant la fonction channel. Comme pour les threads, plusieurs opérations peuvent être effectuées sur les canaus. Le test d'égalité entre deux canaux est effectué avec la commande samechamel. Si un thread veut lire ou écrire un message sur un canal donné, les fonctions rem et send sont respect ivement utilisées. sendpoll et recvpoll sont. des versions instantanées de send et de recv qui retournent NONE lorsque la communication n'est pas immédiatement réalisable. Les fonctions sendevt et recvevt sont des constructeurs d'événements et seront détaillées dans la prochaine section. La circulation de messages sur un canal se fait de manière synchrone. Le message ne passe que lorsqu'un thread est prêt à le recevoir. La table 3.3 montre les opérations qui peuvent être effectuées sur les canawu. 3.2.3 Exemple d'utilisation de threads et de canaux Dans l'esemple 3.1, un thread principal crée un canal et deux fils. Les fils communiquent via le cana1 et chacun imprime un message au début et à la fin. La commande ClO.print est utilisée pour imprimer les messages.
3.2 Primitives de parallélisme 33 signature CHANNEL = sig type 'a chan type 'a event val channel : unit -> 'a chan val samechannel : ('a chan * 'a chan) -> bool val send : ('a chan * 'a) -> unit val rem: 'a chan -> 'a val sendevt: ('a chan 'a) -> unit event val recvevt : 'a chan -> 'a event val sendpoll : ('a chan * 'a) -> bool val recvpoll: 'a chan -> 'a option end Tableau 3.3: Opérations sur les canaux. L'esécution de ce programme consiste en l'impression des différents messages "Bonjour- O", "Bonjour-l", "Bonjour-2". "Au-revoir-l","Au-revoir-2" et "Au-revoir-O" dans un certain ordre. 3.2.4 Événements Les interactions en programmation parallèle se manifestent par la communication et la synchronisation. Le fait que plusieurs processus puissent s'exécuter de manière indépendante peut mener à des résultats très complexes. Pour gérer cette complesité, C ML utilise un mécanisme d'abstraction qui sépare les opérations de synchronisation des mécanismes décrivant les protocoles de communication et synchronisation. CML introduit donc un nouveau type de valeur appelé événement. Les événements fournissent une abstraction de l'implantation des protocoles tout en préservant leur aspect synchrone. Un événement représente une synchronisation potentielle ou latente. Un thread peut se synchroniser sur un événement en appliquant l'opération sync. Il est aussi possible de construire des événements décrivant les opérations dlentrée/sortie sur les canaux avec les fonctions recvevt et sendevt. Ces événements peuvent être utilisés pour la définition des opérations recv et send : val recv = sync O recvevt va! send = sync O sendevt
3.2 Primitives de parallélisme 36 fun communication-simple( ) = let val canal-channel ( ) val pr= ClO.print in pr 'I Bonjour-O \ n" spawn(fn ( ) => (pr "Bonjour-1 \ n"; send (canal. 17) pr " Au-revoir-1 \ n")); spawn(fn ( ) => (pr "Bonjour-2 \ nt'; rem canal; pr " Au-revoir-2 \ nt')); pr " Au-revoir-O \ n" end Tableau 3.4: Exemple d'utilisation de threads et de canaux. 3 2.5 Événements simples Les événements construits à partir de recvevt et sendevt sont appelés événements de base. Ils peuvent également être construits zvec la fonction always qui fournit un événement disponible à tout moment pour la synchronisation, ou avec la fonction joinevt qui fournit un mécanisme de synchronisation à la terminaison d'un autre thread- 11 est possible de construire de nouveaux événements a partir des événements de base en utilisant la fonction wrap. Par exemple, il est possible de transformer un canal d'entiers en un canal de carrés en exécutant le code suivant: wrap (receive ch, fn (x: int) => x*x ) Lors de la synchronisation, Ia valeur lue de ch sera envoyée à la fonction puis son carré sera retourné à la fin. 3.2.6 Cornrnunicat ion sélect ive Afin d'avoir des communications entre plusieurs threads sans inter-blocage, il est nécessaire d'appliquer certaines règles de sélection. CML supporte la communication sélective de manière assez générale en utilisant l'opérateur choose. Cet opérateur permet de choisir un événement prêt pour la synchronisation parmi une liste d'événements divers. L'événement choisi peut contenir à la fois des opérations de lecture et d'écriture. Par exemple un thread peut choisir de lire de l'information de deux canaux de même type comme le montre le code suivant: choose recvevt canal 1,
3.3 Implantation 37 fun communication-simple2( ) = let val canall=channel ( ) and canal2=channel() val pr= CIO-print in pr " Bonjour-O \ n" spawn(fn ( ) => (pr "Bonjour-1 \ n'l; send (canall, 17) pr 'l Au-revoir-1 \ n")); spawn(fn ( ) => (pr "Bonjour-2 \ nt': send (cana12.37); pr If Au-revoir-2 \ n")); sync (choose [ 1) end wrap (recvevt canall. fn- => pr1'au-revoir-0.1\ n' '), wrap (recvevt canal2. fn- => piwau revoir-0.2\ n' ') Tableau 3.5: Exemple d'utilisation des événements et de la sélection. 1 rem Evt cana 12 La fonction wrap peut être utilisée à ce niveau pour permettre au thread de savoir de quel cana1 il a lu. Cette fonction applique un traitement au message reçu sur le canal. Elle retourne une paire contenant le message et le numéro du canal utilisé. choose I wrap (recvevt canall. fn x=> (Lx)). wrap (recvevt canal2, fn x=> (2.4) 1 Le programme de la table 3.5 est basé sur le code de "commnication-simple?' de la section 3.2.3 dans lequel les notions d'événements et de communication sélective ont été rajoutées. -4 l'exécution, ce programme affiche d'abord Bonjour-O puis il envoie 17 sur le canall et 37 sur le canal 2. Puis il choisit l'un des canaux pour la réception. Si le canal 1 est choisi. Au-revoir-l et Au-revoir-0.1 sont affichés. Sinon Au-revoir-2 et Au-revoir-0.2 sont affichés 3.3 Implantation Cette section décrit I'implantation en ML des principales primitives de CML. Le langage CML est entièrement écrit en ML standard et utilise également deux e-xtensions non-
3.3 Implantation 38 standards de SML/NJ: les continuations de première classe et les signaux asynchrones. La première section donne un bref aperçu des continuations de première classe. Les sections suivantes traitent de l'implantation des principales structures CML soient les threads: les canaux et les événements. 3.3.1 Continuations Les continuations constituent une technique pour la programmation par échappement - De ce fait, elles permettent de programmer des applications parallèles entrelacées. Sup posons qu'un processus ait à s'interrompre à un certain moment pour une raison donnée. Le contexte du processus interrompu à un point donné de son exécution peut-être mémorisé dans une continuation. L'invocation de cette continuation permet alors de retourner au point d'interruption du programme et de reprendre son exécution là où elle avait été interrompue. Une continuation est une fonction qui représente le "este du programme'' IRep92al. Le langage Scheme a introduit les continuations au niveau programmation en tant que des valeurs de première classe. Le langage SML/NJ implémente les continuations de première classe via un type abstrait de données 'a cont et deux fonctions principales callcc et throw. type 'a cont val callcc: ('a cont -> 'a ) -> 'a val throw: 'a cont -> 'a -> 'b Fonction callcc La fonction callcc (cal1 with current continuation) prend en argument une fonction f qui elle même prend une continuation en argument. L'expression callcc f applique la continuation courante à f. Si f invoque la continuation courante avec un argument x? cakc f se comporte comme une fonction retournant x comme résultat. Dans l'exemple 3.6 la fonction callcc mémorise l'état du programme au niveau de l'instruction 3. Les instructions 4 et 5 sont la continuation du programme. Elles seront esécutées lors de l'invocation de fn par la fonction throw. Fonction throw La fonction throw prend en argument une continuation k et un argument a. Elle constitue une méthode d'échappement au programme. -4 l'appel de cette fonction, la continuation k est invoquée avec l'argument a. Dans l'exemple 3.7, les instructions 1 à 3 sont esécutées. Après quoi, la continuation fn est invoquée avec l'argument arg. L'esécution du programme continue avec les instruction 4 et 5 du programme A auxquelles l'argument arg est ajouté. Les instructions 4 et 5 du programme A ne sont esécutées que si elles sont appelées par fn.
Programme A Début instruction 1 instruction 2 instruction 3 callcc fn instruction 4 instruction 5 Fin Tableau 3.6: Utilisation de Callcc. Programme B Début instruction 1 instruction 2 instruction 3 throw fn arg instruction 4 instruction 5 Fin Tableau 3.7: Utilisation de throw.
3.3 Implantation 40 Continuations et parallêlïsme '\,IL est un langage séquentiel. CML bien qu'écrit en ML est un langage parallèle et ce passage du séquentiel au parallèle est dû en grande partie a l'utilisation des continuations. Comment simuler le parallélisme avec des callcc et des throw? En réalité. les instructions s'exécutent toujours de façon séquentielle. Les instructions callcc et throw permettent de faire des branchements sur des parties de code. Par exemple l'exécution de deux threads (un producteur P et un consommateur C) en parallèle sur un processeur unique peut être simulée de la manière suivante: au départ P est le thread courant. Pour faire parvenir un message m à C, il active sa continuation en passant m en argument. Le consommateur C devient alors le thread courant. Il stocke le message m et active la contin~ation de P. Le producteur P redevient le thread courant et peut alors continuer son traitement. La figure 3.1 illustre cet exemple. Notons que le parallélisme est obtennu en C griice aux fonctions setjmp et longjmp qui sont équivalents de ca llcc et t hrow. L'instruction setjm p sauveguarde l'état courant du programme alors que l'instruction longjrnp récupère l'environnement. Les signatures de ces fonctions sont les suivantes: int setjmp (jmpbuf env); void longjrnp(jmp-buf env. int value); jm p-buf représente ici un tableau d'environnements....... cailcc(in Pd, throw c ml) -..... - i Producteur P Figure 3.1: Continuations et parallélisme.
3.3 Implantation 41 3.3.2 Implantation des Threads Les threads CML sont représentés par une structure de données Thread-id (voir table 3-2) composée des champs suivants: un entier id servant d'identificateur de thread, un pointeur vers un booléen alert servant à indiquer les alertes, un pointeur vers un booléen done-comm indiquant la fin d'une communication. un pointeur vers une fonction exnhandler pour la gestion des exceptions, une variable de condition dead étant elle même une union de d eu structures! 7 l bool rd: iiai - - Figure 3.2: Structure d'un identificateur de threads. Un Thread-id est obtenu à chaque retour de la fonction spawn. Cette fonction prend en argument une fonction de type (unit+ unit)et retourne un thread-id. La table 3.8 donne le code ML de spawn. -Après être entré dans une zone critique, un nouveau thread-id est créé. La fonction callcc est utilisée ici pour mémoriser le point courant du programme et pour ajouter le thread courant et sa continuation dans rdyql qui est une file d'attente contenant tous les threads prêts à être exécutés. Si une erreur survient lors de l'exécution de la fonction passée en paramètre, alors elle est transmise
3.3 Implantation 42 fun spawnc f x = let val - = S.atomicBegin() val id = newtld() in SMLofNJ.Cont.callcc (fn parentk => ( S.enqueueAndSwitchCurThread(parentK. id): S.atornicEnd(); (f x) handle ex => dohandler(id, ex); noti6anddispatch id)): id end fun spawn f = spawnc f () Tableau 3.8: Fonction spawn. au gestionnaire d'exceptions du thread. Sinon. le champ dead du thread-id prend la..-aleu CVAR-set et la section critique est abandonnée. Le thread-id est retourné en résultat. 3.3.3 Implantation des canaux Les canaux CML sont composés principalement de deux files d'attente InQ et OutQ et d'une priorité (voir table 3.3). La file d'attente lnq recueille les identificateurs et les continuations des threads désirant Iire sur le canal. La file d'attente OutQ recueille les identificateurs et les continuations des threads désirant écrire sur le canal. La priorité est utilisée lors de la synchronisation de l'événement choose pour déterminer le nombre de processus en attente sur le canal. Fonction send La fonction send permet d'envoyer un message sur un canai donné. La table 3.9 en donne l'implantation en ML et son déroulement peut être résumé par les étapes suivantes: 1. Entrer dans la zone critique. '2. Vérifier s'il existe des éléments dans InQ. 3. Dans le cas où il existe au moins un élément:
3.3 Implantation 43 Figure 3.3: Les canaux CML. Enregistrer le point courant du programme dans une continuation sendk. Enlever un élément de InQ et l'enfiler dans rdyq1. Mettre la priorité du canal à 1. Activer la continuation du thread receveur enlevé de InQ en passant le message à envoyer en paramètre. 4. Dans le cas ou InQ est vide: Enregktrer le point courant dans une continuation sendk. Enfiler la continuation courante (celle de l'envoyeur) dans OutQ. Sortir de la section critique. Passer au prochain thread de rdyqi. Fonction recv La fonction recv illustrée par la table 3.10 permet la réception de messages sur un canal donné. Cette fonction prend en argument un canal avec sa priorité et ses files d'attente et retourne un message. La première étape de cette fonction consiste à rentrer dans une zone critique. Le contenu de la file d'attente OutQ est alors vérifié. Si OutQ n'est pas vide alors son élément de tête est défilé. Cet élément contient un identificateur de transaction trans-id ainsi que la continuation du thread envoyeur. 11
3.3 Implantation 44 fun send (CHAN {priority, InQ. OutQ ). msg) = ( S.atornicBegin(); case (cleanandrernove InQ) of Item(rid, rkont) => callcc (fn sendk => ( S.enqueueAndSwitchCurT)iiead(sendK, getldfrorntrans rid); priority : = 1: throw rkont msg)) 1 Noltern => let val (recvld. recvk) = callcc (fn sendk => ( enqueue (OutQ. (mkld(), sendk)): S.atomicDispatch())) in S.atomicSwitchTo (recvld. recvk. rnsg) end Tableau 3.9: Fonction send. s'effectue aiors un changement de contexte: L'identificateur du thread courant (le receveur) est enregistré dans myld et sa valeur est remplacée par trans-id. La priorité du canal est mise à 1 et la continuation du thread envoyeur sendk est activée en prenant le thread courant(1e receveur) et sa continuation en paramètres. Le message est alors transmis de l'envoyeur au receveur. Dans le cas ou OutQ ne contient aucun élément: le thread courant (receveur) et sa continuation sont ajoutés à la file d'attente InQ. La section critique est abandonnée et un autre thread est enlevé de rdyql pour être exécuté. 3.3.4 Implantation des événements Les événements sont implantés par des listes pouvant contenir quatre sortes de valeurs comme le montre le datatype 'a event de la table 3.11. Ces valeurs sont soit: des listes d'événements de base BEVT [pollfnj, des listes d'événements parmi lesquels il faut faire un choix CHOOSE [BEVT [evl, ~V~].BEVT lev31,...i, 0 des fonctions a exécuter GUARD fn ou W-NACK fn. Un événement de base est une fonction pollfn qui prend une valeur de type unit en argument et qui retourne le statut de l'événement. Ce statut est de type event-status
3.3 Implantation 45 fun recv (CHAN {priority, InQ, OutQ )) = callcc (fn recvk => ( S-atomicBegin (); case (cleanandremove OutQ) of Item(transld. sendk) => let val myld = S.getCurThread() in setcurthread transld: priority : = 1; throw sendk (myld. recvk) end 1 Noltem => ( enqueue (InQ, (mkld(). recvk)); S.atornicDispatch()) Tableau 3.10: Fonction rem. qui est une union de deux valeurs: ENABLED ou BLOCKED. La valeur ENABLED est un enregistrement contenant une priorité et une fonction dofn appelée lors de la synchronisation si l'événement est prêt à être synchronisé. La valeur BLOCKED représente la fonction blockfn appelée lors de la synchronisation si l'événement est bloqué et qu'il ne peut être ~~vnchronisé. Lors de la synchronisation d'un événement, la fonction pollfn est d'abord appelée. Cette fonction a pour but de parcourir la liste qui représente l'événement et de retourner un statut pour chaque événement de base rencontré. Si la valeur de retour est ESXBLED, alors l'événement est prêt pour la synchronisation et la sa fonction dofn est appelée. Si par contre la valeur de retour est BLOCKED, alors l'événement ne peut être synchronisé et sa fonction btockfn est appelée. Fonction send Evt La fonction sendevt ( table 3.12) est la version latente de send. Elle prend un canal avec sa priorité et ses files d'attente ainsi qu'un message en argument et retourne une liste d'événements de base BEVT [...] contenant un seul élément et dans lequel les fonctions pollfn, dofn et blockfn sont définies. La table 3.12 illustre son implantation en ML. dofn: cette fonction est appelée si polffn retourne la valeur ENABLED. Elle enlève de la file InQ l'identificateur de transaction ainsi que la continuation du prochain thread receveur. Puis elle place le thread courant (l'envoyeur) dans la file rdyql
3.3 Implantation 46 datatype 'a event-status = ENABLED of {prio : int, dofn : unit -> 'a) ( BLOCKED of { transld : trans-id ref, cleanup : unit -> unit. next : unit -> unit ) -> à type 'a base-evt = unit -> 'a event-status datatype 'a event = BE~T of 'a base-evt lis 1 CHOOSE of 'a event list 1 CUXRD of unit -> 'a event [ W-NACK of unit event -> 'a event Tableau 3.11: Structure des événements. contenant les threads prêts pour l'exécution. La priorité du canal est alors mise à 1 puis la continuation du thread receveur est lancée avec le message à envoyer en argument blockfn: cette fonction est appelée si pollfn retourne Ia valeur BLOCKED. Elle prend en argument l'identificateur de transaction du thread courant (envoyeur). Le thread courant est placé dans la file d'attente OutQ et donne la main au prochain thread de rdyql. pollfn: cette fonction est la première à être appelée lors de la synchronisation des événements. Elle effectue un nettoyage de la file d'attente InQ en enlevant les threads qui ont été annulés (CANCEL). Elle augmente la priorité du canal et retourne la priorité précédente. Si cette priorité vaut O et donc que la file InQ est vide, alors la fonction la valeur BLOCKED est retournée avec la fonction btockfn a exécuter. Si la priorité est différente de 0 aiors la valeur ENXBLED est retournée avec la priorité et la fonction dofn à exécuter. Fonction recvevt La fonction recvevt (table 3.13) est la version latente de rem. Elle prend un canal avec sa priorité et ses files d'attentes en argument et retourne une liste d'événements de base BEVT [...I contenant un seul élément et dans lequel les fonctions pollfn. dofn et blockfn sont définies. La table 3.13 illustre son implantation en ML. dofn: cette fonction est appelée si pollfn retourne la valeur ENABLED. Elle enlève de la file OutQ l'identificateur de transaction ainsi que la continuation du prochain thread envayeur, puis, elle place le thread courant (le receveur) dans la file
3.3 Implanta tion 4'7 fun sendevt (CHAN (priority, InQ, OutQ 3, msg) = let fun dofn () = let val (transld, rkont) = Q-dequeue InQ in callcc (fn sendk => ( S.enqueueAndSwitchCurThread(sendK. getldfromtrans transld); priority : = 1; throw rkont msg)) end fun blockfn transld, cleanllp. next = let val (recvld, recvk) = callcc (fn sendk => ( cfeanandenqueue (OutQ. (transld, sendk)); ne=() ; impossible 0)) in cleanup() ; S.atomicSwitchTo (recvld. recvk. msg) end fun pollfn () = (case (cleanandchk (priority, InQ)) of O => R.BLOCKED blockfn 1 p => R-ENABLED prio=p. dofn=dofn (* end case *)) in R-BEVT (poilfn] end Tableau 3.12: Fonction sendevt.
rdyql contenant les threads prêts pour exécution. La priorité du canal est alors mise à 1 puis la continuation du thread envoyeur est lancée avec l'identificateur du thread courant et sa continuation- blockfn: cette fonction est appelée si pollfn retourne la vdeur BLOCKED. Elle prend en argument l'identificateur de transaction du thread courant (receveur). Le thread courant est placé dans la file d'attente InQ et donne la main au prochain thread de rdyq1. pollfn: cette fonction est la première à être appelée lors de la synchronisation des événements. Elle effectue un nettoyage de la file d'attente OutQ en enlevant les threads qui ont été annulés (CAXCEL). EHe augmente la priorité du canal et retourne la priorité précédente. Si cette priorité vaut O et donc que la file OutQ est vide, alors la valeur BLOCKED est retournée avec la fonction blockfn à exécuter. Sinon, la valeur ENABLED est retournée avec la fonction dofn à exécuter. Fonction choose des des GUARD (fn) OU des W-NACK (fn). La fonction choose effectue un chois parmi une liste d'événements et retourne un événement. L'événement retourné est en quelque sorte un aplatissement de la liste initiale. Si la liste initiale ne contient que des événements de base, alors le résultat de choose sera de la forme: sous avons vu qu'un événement était en fait une liste contenant des BEVT [ 1. CHOOSE [ 1, Si par contre la liste initiale contient des événements autres que des événements de base alors le résultat de choose sera de la forme: CHOOSE [evl, ev2,...] Le chois en tant que tel sera effectué lors de la synchronisation d'un thread sur l'événement retourné. Fonction wrap La fonction wrap prend en argument un événement et une fonction et retourne un év& nement qui sera passé en argument à la fonction lors de la synchronisation. La table 3.15 montre les détails de l'implantation de wrap. S'il s'agit d'un événement de base BEVT[ 1, la fonction wrapbaseevt est appliquée à tous les éléments de la liste. Cette fonction appelle pollfn et si ENABLED est retourné alors la fonction doh de l'événement devient une composition de la fonction passée en argument. Si par contre pollfn retourne BLOCKED, c'est la fonction
fun recvevt (CHANpriority, InQ. OutQ) = let fun dofn () = let val (transld, sendk) = Q-dequeue OutQ val myld = S.getCurThread() in setcurthread transld; priority : = 1; callcc (fn recvk => throw sendk (myld, recvk)) end fun blockfn transld, cleanup. next = let val msg = callcc (fn recvk => ( cleanandenqueue (InQ. (transld, recvk)); next (); impossible())) in cleanup(): S.atomicEnd() ; msg end fun pollfn () = (case (cleanandchk (priority, OutQy)) of O => R.BLOCKED blockfn 1 p => RENABLED prio=p. dofn=dofn (* end case *)) in R-BEVT [pollfn] end Tableau 3.13: Fonction recvevt.
3.3 Implantation 50 fun choose (el : 'a event kt) = let fun gatherbevts (fl, 1) = BEVT I 1 gatherbevts ( BEVT : : r, 1) = gatherbevts (r. 1) ( gatherbevts ( BEVT [bev] : : r, bevs') = gatherbevts ir. bev: : bevs' ) 1 gatherbevts ( BEVT bevs : : r, bevs') = gatherbevts (r. bevs Q bevs') 1 gatherbevts (evts. 0) = gather (evts. 0) 1 gatherbevts (evts. 1) = gather (evts, [ BEVT Il) and gather (O, [evt]) = evt ( gather (0. evts) = choose evts 1 gather ( choose evts : : r, evts') = gather (r, evts Q evts') ( gather ( BEVT bevs : : r, BEVT bevs' : : r') = gather (r, BEVT (bevs @ bevs') : : r') 1 gather (evt : : r. evts') = gather (r. evt : : evts') in gather bevts (rev el. 1) end Tableau 3.14: Fonction choose. blockfn de l'événement qui devient une composition de la fonction passée en paramètre. L'événement retourné est de la forme BEVT de la liste d'événements dont les pollfn ou blockfn ont été modifiés. S'il s'agit d'un CHOOSE[ 1, elle retourne un événement CHOOSE[ 1 dont les événements de base ont été modifiés par la fonction wrapbaseevt de la même manière que dans le cas précédent S'il s'agit d'un GUARD F, elle retourne un événement GUARD f2 dont la fonction f2 est elle même un wrap prenant en argument f et la fonction passée en paramètre. Le traitement de W-XACK F est quasi identique à celui de GC'ARD f. Fonction sync La fonction sync permet l'activation des événements et leur enlève leur aspect latent. La fonction sync peut être considérée comme étact la réunion de trois sous-fonctions principales: sync0noneevt : pour la ~~pchronisation d'un seul événement, synconbevts : pour la synchronisation d'un événement de base, m syncongrp : pour la synchronisation d'un groupe d'événements.
3.3 Implantation 51 fun wrap (evt, wfn) = let fun wrapbaseevt pollfn () = (case pollfn() of ENABLED prio, dofn => ENABLED prio=prio, dofn = wfn O dofn 1 ( BLOCKED blockfn) => BLOCKED (wfn O blockfn) (* end case *)) fun wrap' ( BEVT bevs) =BEVT (map wrapbaseevt bevs) ( wrap' ( CHOOSE evts) = CHOOSE (map wrap' evts) ( wrap' ( GUARD g) = GUARD(fn () => wrap(g(), wfn)) 1 wrap' ( W-NACK f) = W-NACK (fn evt => wrap(f evt, wfn)) in wrap' evt end Tableau 3.15: Fonction wrap. La fonction syncononeevt est appelée pour la synchronisation d'un événement BEVT ne contenant qu'une valeur pollfn. Elle prend en argument la fonction pollfn qui représente l'événement de base. La première étape de cette fonction consiste à entrer dans une section atomique en appelant la fonction atomicbegin. Puis, selon que pollfn retourne ENABLED ou BLOCKED les fonctions dofn ou blockfn de l'événement à synchroniser sont appelées. La table 3.16 donne l'implantation en ML de syncononeevt. La fonction synconbevts est appelée lorsqu'il faut synchroniser des listes d'événements de base. Les détails de cette fonction sont donnés par la table 3.17. Cette fonction exécutée sur une liste vide lance le prochain thread en attente sur rdyq1. Lorsqu'une liste contenant un seul élément est passée en paramètre, la fonction syncononeevt détaillée dans la section précédente est appelée. Dans le cas d'une liste contenant plusieurs événements de base, la fonction ext est exécutée. Cette fonction a pour rôle de parcourir la liste et d'appliquer pollfn a chaque événement. Si la valeur BLOCKED est retournée alors cette vaieur est emmagasinée dans une liste et l'on passe au prochain élément. Si la valeur ENABLED est retournée, alors ext termine et une autre fonction extrdy se charge de mettre dans une liste tous les événements prêts pour la synchronisation. Dans le cas où aucun élément de la liste n'est ESXBLED ext termine en retournant une liste contenant les BLOCKED. Cne fonction selectdofn se charge alors de sélectionner un événement parmi les éléments prêts à être synchronisés. Elle retourne le dofn qui sera appliqué. La sélection est basée sur la priorité. L'événement ayant la priorité la plus élevée est sélectionné. Si la priorité maximale est partagée entre plusieurs évéuements, le choix est alors aléatoire. La fonction syncongrp traite le cas des groupes d'événements W-NACK.
3.4 Conclusion 52 fun syncononeevt (pollfn : 'a base-evt) = ( S-atomicBegin (); case (pollfn()) of ENABLED dofn,... => dofn() 1 (BLOCKED blockfn) => let val (flg, setflg) = mkflg() in blockfntransld=flg. cleanup=setflg. next=s.atomicdispatch end (* end case *)) Conclusion Tableau 3.16: Fonction syncononeevt. Ce chapitre décrit le fonctionnement de la programmation parallèle en CML ainsi que son implantation. Les threads CML communiquent entre e u via des canaux typés qui gèrent les aspects de synchronisation. CML introduit la notion d'événements qui sont des communications latentes activées grâce à la fonction sync. Ces événements peuvent êtres combinés ou modifiés avant leur activation ce qui permet une programmation beaucoup plus souple. Parmi les opérations possibles sur les événements, nous pouvons citer le choix qui permet de choisir parmi une liste d'événements un candidat prêt pour la synchronisation. L'implantation du langage CML est basée sur les continuations. qui sont une estension de SML/NJ (Standard ML of New Jersey). Les continuations. en permettant la programmation par entrelacement et la programmation par échappement, constituent un outil très puissant pour l'implantation de langages parallèles. Le chapitre suivant est une étude comparative des deux langages parallèles vus dans les chapitres 1 et 2 à savoir Java et CrVlL. Il fait ressortir les défauts de parallélisme du langage Java et motive la conception et l'implantation d'un nouveau paquetage de threads pour ce langage.
3.4 Conclusion 53 fun synconbevts fl = S.dispatch() 1 synconbevts [bev] = syncononeevt bev 1 synconbevts bevs = let fun ext (0. blockfns) = capture (fn k => let val escape = escape k val (transld, setflg) = mkflg() fun log O = S-atomicDispatch () 1 log (blockfn : : r) = escape (blockfn ( transld = transld. cleanup = setflg, next = fn () => log r 1) in log blockf ns; error "[log] If end) 1 ext (pollf n : : r, blockf ns) = (case pollfn() of ENABLEDprio. dofn => extrdy (r, [(prio, dofn)]. 1) 1 (BLOCKED blockfn) => ext (r. blockfn:: blockfns) (* end case *)) and extrdy ([j, dofns. n) = selectdofn (dofns, n) () 1 extrdy (pollfn : : r. dofns. n) = (case pollfn() of ENABLEDprio, dofn => extrdy (r, (prio, dofn): : dofns, n+l) 1 - => extrdy (r. dofns, n) (* end case *)) in S.atomicBegin(); ext (bevs. O) end Tableau 3.17: Fonction synconbevt.
Chapitre 4 Étude comparative 4.1 Introduction Dans ce chapitre, nous évaluons respectivement les composantes parallèles de Java et de CML selon un certain nombre de critères tels : la communication: le transfert de données entre deux ou plusieurs processus selon certaines conventions. La synchronisation: nécessaire lorsque plusieurs processus communiquent entre eux et accèdent a des ressources partagées. L'ordonnancement: pour la gestion de l'allocation du processeur au différents processus à esécuter. Les abstractions: qui permettent d'isoler certains aspects importants d'un problème en ignorant les détails moins importants. 0 La portabilite: qui permet l'exécution d'un même programme sur différentes plates-formes. La sémantique formelle: qui décrit le comportement d'un programme..au niveau statique elle permet de le typer et au niveau dynamique elle permet de l'évaluer. La performance: qui permet de juger un programme selon ses résultats. 4.2 Évaluation de Java Java est un langage de programmation très en vogue de nos jours car il est particulièrement adapté aux besoins actuels. C'est entre autres un langage orienté objet simple, robuste, sécurisé et parallèle. Cependant, L'évaluation de sa composante parallèle fait ressortir un certain nombre de lacunes à plusieurs niveaux au niveau de l'ordonnancement,
4.2 Évaluation de Java 58 a au niveau de la portabilité, a au niveau de la communication, 0 au niveau de la synchronisation, au niveau de la sémantique formelle, au niveau des abstractions, a au niveau de la performance. 4.2.1 Ordonnancement La notion de priorité a été introduite au niveau de la programmation temps-réel et parallèle de façon à ordonner I'exécution des processus: les priorités les plus élevées l'emportant sur les plus faibles. En Java, la politique de gestion de la priorité n'est pas clairement spécifiée. Sous les threads en java ont une priorité. Lorsqu'il y a compétition entre différents threads pour l'acquisition des ressources système, les threads de priorité plus élevée sont généralement exécutés les premiers. Ce comportement n'est toutefois pas garanti. L'une des règles "Rule of thumb" tirée du "Java Tutorialy'[CW981 est définie comme suit: RGLE OF THUMB: "At any given time. the highest priority thread is x-unning. However, this is not guaranteed. The thread scheduler may choose to run a lower priority thread to avoid starvation. For this reason, use priority only to affect scheduling policy for efficiency purposes. Do not rely on thread priority for algorithm correctness." Il n'est par conséquent pas conseillé de compter sur la priorité des threads pour implanter de manière fiable des algorithmes d'exclusion mutuelle entre processus. 4.2.2 Portabilité Tout code Java est d'abord compilé vers un code objet intermédiaire dit "bytecode". Ce code est par la suite interprété par une machine virtuelle. Cette compilation à deux phases devrait, entre autres, permettre une parfaite portabilité du langage Java. En d'autres termes, la nouvelle architecture définie par Java devrait permettre de passer à travers les différences des systèmes et offrir un mécanisme totalement indépendant de la plate-forme utilisée. Cependant, la considération du volet parallèle de Java ajoute un bémol a ces objectifs. En effet, la source du problème est que l'ordonnanceur de la machine virtuelle de
4.2 Évaluation de Java 56 publicclass SelfishRunner extends Thread private int tick = 1: private int num; public SelfishRunner(int num) ( this-num = num; 1 publi4void run() while (tick < 400000) { tick++; if ((tick % 50000) == 0) System-out,println("Thread #" + num + If. tick = " + tick); 1 public class RaceTest { private final static int NUMRUNNERS = 2; public static void main(string1 args) { SeIfishRunnerO runners = new SelfishRunner[NUMRUNNERS]; for (int i = O; i < NUMRUNNERS; i++) { runners[i] = new SelfishRunner(i) ; runners[i] setpriority(2); 1 for (int i = O; i < NUMRUNNERS; i++) runners[i].start(); Tableau 4.1: Exécution de threads à priorité égale. Java est fortement dépendant du système d'exploitation. Les systèmes d'exploitation Windows 95/98/NT utilisent une politique de temps partagé (time slicing) pour la gestion des processus et leur multiplexage sur les processeurs disponibles. En contre partie. les systèmes de type SunOs/Solaris utilisent une politique d'ordonnancement préemptif, c'est à dire qu'un thread s'exécute jusqu'à sa complétion ou jusqu'à ce qu'un autre thread de priorité supérieure soit prêt à s'exécuter. De ce fait, une application Java peut avoir des comportements différents selon la plate-forme d'exécution. Considérons le code de la table 4.2.2 tiré du "Java Tutorial" de Sun [CW98] qui crée deus threads de priorité égale. L'exécution de ces threads est en somme une incrémentation de "ticks" jusqu'à ce qu'une certaine condition soit verifiée. L'exécution sur un système de type Windows est la suivante:
4.2 Évaluation de Java 57 Thread #1, tick = 50000 Thread #O, tick = 50000 Thread #O. tick = 100000 Thread #1. tick = 100000 Thread #1. tick = 150000 Thread #1. tick = 200000 Thread #O. tick = 150000 Thread #O. tick = 200000 Thread #l, tick = 250000 Thread #O, tick = 250000 Thread #O. tick = 300000 Thread #1, tick = 300000 Thread #1, tick = 350000 Thread #O. tick = 350000 Thread #O, tick = 400000 Thread #1. tick = 400000 Par contre sur un système de type SunOs/Solaris l'exécution est: Thread #O, tick = 50000 Thread #O, tick = 100000 Thread #O, tick = 150000 Thread #O, tick = 200000 Thread #O, tick = 250000 Thread #O. tick = 300000 Thread #O, tick = 350000 Thread #O. tick = 400000 Thread #1, tick = 50000 Thread #1, tick = 100000 Thread #1. tick = 150000 Thread #l. tick = 200000 Thread #1, tic& = 250000 Thread #l. tick = 300000 Thread #1, tick = 350000 Thread #1. tick = 400000 Pour remédier à ce problème, les concepteurs de Java suggèrent l'emploi de la primitive yield. Si l'utilisation de cette méthode permet d'uniformiser le comportement des threads selon les systêmes utilisés, elle prouve par la même occasion qu'un code écrit sous Windows n'aura pas forcément la même exécution sur un système SunOs/Solaris et vice versa et donc que le langage Java n'est pas à 100 pour-cent portable. Il est important de noter que l'utilisation de yield ne constitue une solution que dans la mesure où nous sommes en présence d'un nombre réduit de threads. En effet, la responsabilité de gestion est remise au programmeur et ce dernier ne peut l'endosser s'il a affaire à un grand nombre de processus légers s'exécutant en parallèle et se synchronisant suivant des schémas compliqués.
3.2 Évaluation de Java 58 4.2.3 Communication Pour communiquer entre eux, les threads Java font transiter des informations via des objets appelés objets partagés ou variables partagées. Au niveau de ces objets, les portions de code utilisées par plusieurs threads sont appelées sections critiques. La programmation de ces sections critiques en Java s'avère particulièrement délicate. Il faut en effet s'assurer qu'aucune interférence entre les threads n'en résultera. La déclaration d'une section critique se fait à l'aide du mot clé synchronized. Ces sections consistent en des mécanismes de synchronisation de contrôle qui permettent a u processus d'y accéder sans risque de conflit. La gestion explicite de ces conflits génère une complexité additionnelle lors de la programmation. 4.2.4 Synchronisation Les mécanismes de synchronisation utilisés par Java pour empêcher l'accès simultané de plusieurs threads à une section critique sont appelés moniteurs. Tout objet Java possède un verrou et le rôle des moniteurs consiste en l'activation ou la désactivation des ces verrous dans des situations particulières. Le mot clé synchronized indique qu'un moniteur doit être acquis. Il génère une référence a un objet et tente d'activer un verrou sur cet objet. Une fois l'objet verrouillé, le corps du code synchronized est exécuté. -4 la fin de l'exécution, l'objet est automatiquement déverrouillé- Les méthodes wait, notify et notifyall permettent de manipuler le verrou à l'intérieur d'une méthode s~chronised. La méthode wait est appelée alin de désactiver provisoirement le verrou d'un objet. Cet objet reste déverrouillé jusqu'à ce qu'il soit notifié par un autre thread ayant utilisé la méthode notih ou notisall. La méthode notisl réactive arbitrairement le verrou d'un des threads ayant appelé la méthode mit. Pour avoir la certitude qu'un verrou spécifique a été réactivé, la méthode notifyall est plus adaptée. Elle réactive automatiquement tous les verrous désactivés provisoirement par une méthode wait. Les méthodes wait et notify/notifyall s'apparentent a u primitives P et V des sémaphores conventionnels qui ne constituent pas les outils les plus faciles pour la programmation des aspects de synchronisation. De plus, les interactions entre ces primitives et les moniteurs ne sont pas clairement spécifiées par les concepteurs du langage. Le fonctionnement de notify/notihall nous en donne une preuve. 4.2.5 Sémantique formelle Lors de sa conception, Java n'a pas bénéficié d'une définition formelle. La définition sémantique du langage est donnée sous forme de prose. Cette absence de définition formelle fait que plusieurs questions sur la sémantique du langage restent sans réponse. Par esemple, certains aspects tels que: le typage qui consiste à déterminer le type d'une expression ou savoir si un programme est bien typé,
4.3 Évaluation de CML 59 l'exécution qui consiste à déterminer le résultat de l'évaluation d'une expression donnée, la propagation des exceptions, les liens d'héritage, la création et l'initialisation d'objets, demeurent relativement flous. 4.2.6 Abstractions les abstractions constituent un outil très efficace en programmation parallèle. Elles permettent entre autres des synchronisations et autres comportements latents. Les primitives de programmation parallèle en Java n'englobent pas ces abstractions. 4.2.7 Performance Le fait que Java soit un langage interprété autant que compilé offre de nombreux avantages comme une meilleure portabilité, une meilleure sécurité et des fichiers de taille relativement faible. Cependant, l'interprétation est coûteuse en temps. De ce fait, Java souffre d'importants problèmes de performance car la couche supplémentaire entre l'application, le système d'exploitation et la machine utilise énormément de ressources système. Certains autres aspects propres à la programmation affectent également les performances de Java. Nous pouvons citer: l'utilisation des gestionnaires d'exceptions: les blocs try/catch utilisés pour la gestion des exceptions sont assez lents a exécuter; la concaténation d'objets de type String ou StringBuffer: De même la concaténation des objets de type String ou StringBuffer entraîne la création de nouveaux objets qui seront par la suite copiés puis modifiés suivant un mécanisme ralentissant l'exécution; l'utilisation des méthodes synch ronized: lors de la programmation de Threads. l'utilisation de méthodes synchronized ralentit beaucoup le processus d'exécution, l'acquisition des verrous étant assez lente: 4.3 Évaluation de CML Afin de faciliter la comparaison des deux langages CML et Java, nous allons évaluer CM L selon les mêmes critères que Java soit:
4.3 Évaluation de CML 60 au niveau de la communication. 0 au niveau de la synchronisation, au niveau de l'ordonnancement. au niveau des abstractions, au niveau de la portabilité, au niveau de la sémantique formelle. au niveau de la performance. 4.3.1 Communication La communication en CML se fait par passage synchrone de message. Le transfert de messages se fait par l'intermédiaire de canaau en faisant appel a deux fonctions principales: l'envoi et la réception. Faciles d'utilisation, les canaux ne nécessitent aucune gestion explicite des ressources pour assurer leur intégrité comme c'est le cas dans la communication par variables partagées. De plus, l'accès en exclusion mutuelle ailu primitives d'envoi et de réception de messages sur les canaux est assuré par l'environnement d'exécution. Les canaux constituent un média sûr de communication et ce, grâce à un typage fort. Sur un canal CML, il n'est pas possible d'envoyer ni de recevoir de données si elles ne sont pas conformes aux types préalablement déclarés ou inférés. L'utilisation des canaux est d'autant plus facilitée qu'ils sont créés dynamiquement. Ce dernier point ajoute une touche de Aéxibilité à la programmation des communications en CML. 4.3.2 Synchronisation La synchronisation en CML est réalisée par l'application d'un mécanisme de rendezvous selon lequel les processus envoyeurs (respectivement les processus receveurs) se bloquent jusqu'à ce que les processus receveurs (respectivement les processus envoyeurs). soient prêts. Le langage CML offre plusieurs formes de synchronisation: La synchronisation sur des actions complémentaires sur des canaux qui est obtenue par l'exécution des fonctions send et recv. C'est une synchronisation bi-points et bloquante. La synchronisation sur la terminaison d'un autre processus. 0 La synchronisation sur une condition qui peut être obtenue par l'utilisation de la fonction guard.
4.3 Eva~uation de CML 61 4.3.3 Ordonnancement Le mécanisme d'ordonnancement de CML est un mécanisme pré-emptif. Les processus s'esécutent jusqu'à leur complétion ou jusqu'à leur pré-emption par un processus de priorité supérieure. De plus, ce mécanisme est implicite c'est-à-dire qu'aucune gestion supplémentaire de l'ordonnancement n'est nécessaire de la part du programmeur pour s'assurer de l'ordre d'exécution des threads. La pré-emption assure un changement de conteste selon des intervalles réguliers et empêche la monopolisation du processeur par un seul thread. 4.3.4 Abstraction L'une des contributions de CML que l'on peut qualifier à la fois d'originale et d'innovatrice est l'introduction de la notion d'événements. Cette notion permet de définir des abstractions sur le volet parallèle du langage. De la même façon qu'une fonction abstrait un calcul séquentiel, la notion d'événements permet d'abstraire des synchronisations et des communications. De plus, du fait que CML soit bâti au dessus de SML, il hérite de toutes les abstractions offertes par ce langage telles que les abstractions fonctionnelles : les fonctions. les fonctions d'ordre supérieur, a les X expressions. Ce type d'abstractions permet une programmation aisée. claire et compacte et réduit considérablement les efforts du programmeur lors de l'élaboration de ses applicat ions. En introduisant les opérations synchrones de première classe, CML fournit un mécanisme puissant pour l'implantation d'abstractions de communications et de synchronisat ion. Ces abstractions permet tent de séparer la descript ion de la communication et de la synchronisation de leur application effective. 4.3.5 Portabilité Le langage CML est portable. Écrit en SML, il s'exécute actuellement sur quatre architectures différentes et plusieurs systèmes d'exploitation supportés par SM L/NJ. 4.3.6 Sémantique formelle Une littérature abondante existe sur la définition de la sémantique formelle de CML. Ces différents travaux couvrent l'aspect syntaxique (grammaire), statique (système de types) et dynamique (sémantique opérationnelle et dénotationelle). A titre d'exemples nous pouvons citer pour la sémantique statique: Debbabi, Thomsen et Nielson et pour
4.4 Conclusion 62 la sémantique dynamique: Debbabi. Ferreira, Hennessy, Jeffery, Neilson et Thornsen[DebS-il. En outre, de nombreuses analyses statiques de programmes ont été effectuées sur CML. Plusieurs travaux dont ceux de B-Thomsen et Nielson ont contribué à l'élaboration de son système de types basé sur des extensions de la discipline de types. En particulier, une extension du A calcul appelée X cv introduite par J.H. Reppy qui modélise la plupart des fonctions de CML. 4.3.7 Performance Une série de tests effectués sur différentes machines et différents processeurs a permis de démontrer l'efficacité du langage CML. Il combine à la fois des notations de haut niveau très flexibles avec une bonne performance. 4.4 Conclusion L'évaluation de la composante parallèle de Java a permis de faire ressortir les principales lacunes de son paquetage de threads: la politique de gestion de la priorité n'est pas clairement spécifiée, la portabilité varie avec le système d'exploitation utilisé. la communication et la s~vnchronisation sont obtenues à partir de variables partagées dont la gestion est compliquée, la sémantique formelle et les abstractions font défaut et il existe des problemes de performance. Évalué selon les mêmes critères. le langage CML s'avère être un excellent langage parallèle. bmalheureusernent, il reste un langage très peu utilisé. d'où l'idée de transférer ses principes de fonctionnement à un langage plus populaire. en l'occurrence Java. Dans le chapitre suivant, nous proposons un nouveau paquetage de threads pour Java basé sur le fonctionnement de CML.
Chapitre 5 Vers un nouveau paradigme de programmation parallèle en Java 5.1 Introduction Parmi les problèmes soulevés dans la section 4, la gestion de La communication. la gestion de la synchronisation et l'absence d'abstractions sont des plus critiques lorsqu'il s'agit d'un langage de programmation parallèle. Dans le but de faciliter ces aspects de la programmation en Java, nous avons développé un paquetage permet tant l'utilisation de canaux pour la communication et la synchronisation ainsi que l'utilisation d'événements qui sont des abstractions de communications. Ce paquetage HOJ (Higher Order Java) se veut une version allégée et adaptée au langage Java du concept de "Higher Order Concurrencf' introduit par John. H. Reppy au niveau de CML (Rep92al. Plusieurs travaux ont été effectués dans le but d'incorporer le concept de canaux au langage Java. Nous pouvons citer notamment les travaux de Gerald Hilderink. Jan Broenink. Wiek Vervoort et André Bakkers (HBVB971, de Paul Austin [Ausl et d'eric Demaine [Deml. L'avantage de la communication par canaux est qu'ils encapsulent à la fois la communication, la synchronisation, et une partie de l'ordonnancement ce qui facilite énormément la programmation. Les événements, de par leur propriété de pouvoir se combiner les uns avec les autres, permettent de manipuler les communications avant leur exécution en fonction de paramètres parfois inconnus à la compilation. 5.2 Architecture Cette section décrit I'architect ure des principales composantes du paquetage HO J soient : les threads. les canaux, les événements.
Elle donne une vue générale sur la structure des principales classes utilisées et leur hiérarchie. 5.2.1 Threads Le paquetage HOJ utilise les threads Java. Toutefois, dans le souci d'une implantation la plus proche possible du langage CML, une nouvelle ckisse HojThread contenant des méthodes correspondant à toutes les fonctions relatives aux threads CML a été définie. En plus des threads à fonctionnalités générales, des threads spécialisés sont disponibles au niveau du paquetage HOJ: les threads envoyeurs dont la seule fonction consiste en l'envoi d'un message unique sur un canal, les threads receveurs dont la seule fonction consiste en la réception d'un message unique sur un canal. Ces threads spécialisés sont obtenus par le sous-classement de HojThread qui est ellemême sous-classe de java.lang.thread. La figure 5.1 illustre la hiérarchie existante entre ces classes. Comme les threads CML, les HojThreads sont dotés d'un identificateur de thread. ou Thread-Id, qui est retourné par la méthode spawn. La méthode spawn est en fait une surdéfinition de la méthode start des threads Java. Toutes les opérations sur cet identificateur ont également été implantées. Figure 5.1: Hiérarchie des threads.
5.2 Architecture 63 Canaux Le paquetage HOJ ajoute la notion de canaux au langage Java. Les canau fournissent des mécanismes pour la communication par passage de message. La communication des Threads Java se fait à l'aide de variables partagées. L'accès à ces variables doit être géré manuellement de façon à sauvegarder l'intégrité des ressources. Cette gestion implique la création de moniteurs en utilisant des méthodes synchronized ainsi que leur activation et désactivation grâce aux méthodes wait, notify et notisall. Elle implique également la programmation des méthodes pour l'envoi et la réception des messages. Les canaux HOJ permettent d'abstraire les mécanismes de communication. de synchronisation et d'ordonnancement de Java. Basés sur les canailu CML. ils sont par défaut de type rendez-vous, c'est-à-dire que les processus d'envoi et de réception s'attendent mutuellement: un thread envoyeur restera bloqué tant qu'un thread receveur n'aura pas intercepté son message. De même? tant qu'un thread envoyeur n'aura envoyé aucun message, le thread receveur restera bloqué. Outre les canaux de type rendez-vous, CML fournit d'autres outils de cornmunication tels: les mailbox qui sont des canaux bufferisés non bornés, 0 les Ivars qui sont des variables de conditions unidimensionnelles. les Mvars qui sont des variables de conditions unidimensionnelles pouvant être mises à jour plusieurs fois. La principale différence entre ces outils est le moyen qu'ils utilisent pour stocker les données et la façon d'y accéder. Les canaux HOJ sont structurés de manière a permettre l'utilisation de ces outils avec n'importe quel canal. La figure 5.2 décrit leur architecture. Une classe abstraite Channel est reliée a une classe DataStore du même type. Ces classes contiennent les champs et les déclarations de méthodes que doivent implanter toutes leurs sous-classes. Tout canal HOJ est relié à une unité de stockage DataStore qui regroupe les types de communication rendez-vous, mailbox, lvar et Mvar. Nous aurions pu nous contenter d'un canal à travers lequel il serait possible de faire passer des objets de n'importe quel type sans tenir compte de ce dernier. Cependant. une telle conception des canaux irait à l'encontre du principe de ML et de CML où toutes les valeurs sont fortement typées. De ce fait, au dessous de la classe Channel, nous avons créé des canaux correspondant a chaque type primitif de Java et un canal pour les types non prédéfinis. Ceci à permis d'obtenir des canaux non pas typés mais "serni-typés" car il aurait été impossible de créer une classe pour tout nouvel objet. Les canaux HOJ respectent égaiement la propriété de mobilité des canaux CML, c'est à
dire qu'à tout moment un canal peut être eavoyé sur un canal. - - - -- - - - Figure 5.2: Architecture des canaux. 5.2.3 Événements Les événements représentent des synchronisations potentielles ou latentes. Au lieu d 'écrire des fonctions qui déclenchent l'exécution des communications, il est possible d'écrire des fonctions qui retournent des événements décrivant ces communications. De ce fait. leur structure représentative doit pouvoir contenir toutes les données nécessaires à leur activation éventuelle. La structure des événements HOJ illustrée par la figure 5.3 s'apparente le plus possible à celle des événements CML. Un événement CML est une union de trois t4vpes de données: BEVT : un tableau d'événements de base, en sachant qu'un événement de base est composé de trois fonctions (voir section 3.3):
5.3 Implantation 67 - blockfn rm CHOOSE : un tableau d'événements GUARD : une fonction retournant un événement Sous avons représenté un événement par une classe contenant entre autres trois données privées, BEVT, CHOOSE et GUARD, ainsi qu'un certain nombre de méthodes. BEW CHOOSE CU- BEVT / CEOOSE EVENEMENT \ Figure 5.3: Structure des événements. 5.3 Implantation Cet te section décrit les principales méthodes utilisées pour l'implantation des canaux et des événements CML en Java. Notamment, les méthodes send et recv de la classe Channel qui sont les gestionnaires de la communication, de la synchronisation et de l'ordonnancement sur les canaux et leurs actions sur les différentes unités de stockage. Le fonctionnement des méthodes sendevt et recvevt des événements ainsi que de la rnét hode guard est également détaillé. 5.3.1 Canaux L'implantation des canaux HOJ repose sur une classe Channel qui définit les méthodes send et recv responsables de l'envoi et de la réception de messages sur les canaux, ainsi
que les méthodes sendevt et recvevt qui en sont les versions latentes. Ces deux dernières méthodes seront e-upliquées en détail dans la section 5.3.3. Méthode send La méthode send permet l'envoi de messages sur un canal donné. Elle gère les aspects de synchronisation et de communication du côté de l'envoyeur. Les canaau HOJ sont 'plusieurs-à-plusieurs", c'est à dire qu'ils sont conçus pour supporter plusieurs envoyeurs en entrée et plusieurs receveurs à la sortie. Ceci implique une double utilisation de moniteurs pour sauvegarder l'intégrité des ressources. En effet. s'il s'agit de canaux "un-à-un", un seul moniteur sur l'objet canal est nécessaire et son fonctionnement est le suivant: Lors de l'appel de la méthode send, le moniteur sur le canal est activé, bloquant ainsi l'accès à tout autre thread désirant recevoir. Lors de l'appel de la méthode wait, ce moniteur est momentanément désactivé pour permettre au receveur d'accéder à la méthode recv et est automatiquement réactivé par la méthode notiqall. Un tel schéma convient parfaitement lorsqu'il n'existe qu'un envoyeur et qu'un receveur. 11 est toutefois facile de constater que l'ajout de threads envoyeurs et receveurs supplémentaires pourrait résulter en une mauvaise gestion des données envoyées et reçues sur le canal. En effet, lors de la désactivation du moniteur par la méthode wait, un autre thread peut à son tour appeler la méthode send et remplacer te premier message par un autre avant qu'il n'ait pu être reçu. Pour éviter ce problème, deux moniteurs sont activés lors de l'appel de la méthode send. La figure 5.4 illustre ce cas. Le premier moniteur, le moniteur des envoyeurs, est activé lors de I'appel de la méthode send et est désactivé à sa terminaison. Les méthodes wait et notisr sont sans action sur son verrou qui reste de ce fait activé jusqu'à la fin du send. Le deuxième moniteur, le moniteur du canal, est également activé lors de l'appel de Ia méthode send et désactivé à sa terminaison. 11 peut cependant être momentanément désactivé par la méthode wait et permettre ainsi au thread receveur d'accéder à la méthode rem. Le véritable transfert de données a lieu via le Datastore. C'est en effet dans cette unité de stockage que sont conservés les messages. La méthode send se déroule en suivant les étapes suivantes: 1. Envoi du message dans l'unité de stockage. 2. Notification du receveur. 3. Si l'unité de stockage est remplie alors attente jusqu'à ce que son contenu soit récupéré par un receveur.
5.3 Implantation Les lignes de code ci-dessous permettent de créer un canal d'entiers ch et d'y envoyer la valeur 10: Datastore ds = new Rendezvous(); Chan-of-lnteger ch = new Chan-of-lnteger("ch",ds); ch.send(l0); e i I'unitë'de stockage est remplie do attente jusqu'a ce qu'un receveur - - - -~ Moni~cw du amai (~ctive'lon& l'appel & ka nu'liodc soui() et duactive ' Iors & l'onmre d'un receveur oua&fm&&mckic) Figure 5.4: méthode send. Méthode recv La méthode recv est quasi identique à la méthode send. Au lieu de permettre l'envoi des données, elle permet la réception de messages et gère la communication et la synchronisation du côté du receveur. Pour des motifs identiques à ceux établis dans la section précédente, la méthode recv utilise d eu moniteurs. Le premier moniteur, le moniteur des receveurs, est activé lors de l'appel de la méthode recv et est désactivé a sa terminaison. Les méthodes wait et notify sont sans action sur son verrou qui reste de ce fait activé jusqu'à la fin du recv. Le deuxième moniteur, le moniteur du canal, est également activé lors de l'appel de la méthode recv et désactivé à sa terminaison. II peut cependant être momentanément désactivé par la méthode wait et permettre ainsi au thread envoyeur d'accéder à la
5.3 Implantation 70 méthode send. La méthode recv se déroule en suivant les étapes ci-dessous: 1. Si l'unité de stockage est vide alors attente jusqu'à ce qu'elle soit remptie par un envoyeur 2. Récupération du message dans l'unité de stockage 3. Xotification de l'envoyeur Les lignes de code ci-dessous permettent de créer un canal d'entiers ch et d'y recevoir une certaine valeur: lnteger valeur; Datastore ds = new Rendezvous(); Chan-of-lnteger ch = new Chan-of-lnteger("ch",ds); valeur=ch.recv(): 5.3.2 Unités de stockage Les unités de stockage ou Datastores sont des objets associés aux canaux Ils sont responsables du stockage des messages et de leur transfert en tant que tel à travers les canaus qui eux s'occupent plus spécialement des problèmes de synchronisation. L'avantage de ces unités de stockage est qu'elles permettent de choisir le mode ou la manière dont les messages seront stockés et récupérés ainsi que le type de la cornmunication. Les méthodes send et recv auront donc des comportements différents suivant l'unité de stockage utilisée. Toutes les unités de stockage possèdent une variable state qui indique leur état courant. Cette variable peut prendre trois valeurs différentes: FULL : lorsque l'unité de stockage est remplie. EMPTY : lorsque l'unité de stockage est vide. ISBETWEEN : lorsque l'unité de stockage contient des éléments mais qu'elle n'est pas remplie pour autant. Les valeurs que peut prendre la variable state dépend de l'unité de stockage utilisée. Par exemple: les unités de type rendez-vous, lvar et Mvar ne peuvent avoir que deux états: FULL et EMPTY, tandis que Ies unités de type mailbox peuvent se retrouver dans l'état IXBETWEEN.
Rendez-vous Les unités de stockage de type rendez-vous sont utilisées pour obtenir des communications rendez-vous. C'est-à-dire, des continuations dans lesquelles les processus s'attendent mutuellement. Dans ce type de communication, un thread envoyeur reste bloqué jusqu'à ce qu'un receveur récupère son message. De même, un thread receveur reste bloqué jusqu'à ce qu'un envoyeur dépose un message dans l'unité de stockage. Les unités de stockage de type rendez-vous peuvent avoir deux états: EMPTY ou FLLL. La figure 5.5 montre l'action des fonctions send et recv sur les unités de stockage rendez-vous. -- -- -- -- - - - Figure 5.5: États des unités de stockage de type rendez-vous. Mailbox Les unités de stockage de type mailbox permettent d'implanter le passage asynchrone de message ou encore la communication bufferisée représentée en CML par la structure mailbos. Lorsqu'un canal est associé à une unité de stockage de ce type. la méthode send est non bloquante et la variable state ne contient jamais la valeur FULL. Lorsqu'un thread envoie un message sur une mailbox, si cette dernière est vide le message est stocké en première position et la variable state passe de EMPTY à INBETWEEN. Si par contre la mailbox n'est pas vide alors le message est stocké à la suite des autres messages. Lors de la réception, donc de l'appel de ma méthode rem, le premier message arrivé est récupéré et le second prend sa place. La figure 5.6 montre l'action des fonctions send et recv sur les unités de stockage de type mailbox. lvar et Mvar Les unités de stockage de type lvar et Mvar permettent I'implantation de la structure SyncVar de CML. Ce sont des variables synchrones encore appelées cellules de mémoire. EIle peuvent se retrouver dans deux états distincts: EMPTY ou FULL. La figure 5.7 montre les différents états des variables de conditions. Une fois remplies à l'appel de la méthode iput, les Ivars conservent le message de façon permanente. Leur variable state passe de E'VIPTY a FULL et reste dans cet état méme après l'appel de la méthode iget qui Iit la valeur stockée. Les Ivars ne peuvent
5.3 Implantation 72 Figure 5.6: États des unités de stockage de type mailbox. êt.re vidées une fois initialisées. Contrairement aux Ivars, les Mvars peuvent être mises a jour plusieurs fois. Cette possibilité de mise a jour constitue la principale différence entre ces deux types de variables de condition. Lors de l'appel de la méthode mput, le message est stocké dans la variable. Si cette variable était déjà remplie alors une exception serait lancée. La méthode mtake récupère le message et remet la variable state à EMPTY. 5.3.3 Événements L'implantation des événements HOJ a nécessité la création de quatre classes principales: Event: Base-Event: Sending; Receiving; La classe Event définit la structure des événements ainsi que les méthodes nécessaires a leur création et a leur synchronisation à l'exception des classes sendevt et recvevt qui sont définies au niveau de la classe Channel. Nous avons implanté la version simplifiée des événements CML qui sont une union de trois types représentée par le datatype suivant: datatype 'a event = BEVT of 'a base-evt list 1 CHOOSE of 'a event lis ( GUARD of unit-> 'a event
5.3 Implantation 73 Figure 5.7: États des variables de conditions de type lvar et Mvar.
5.3 Implantation 74-4 défaut de la structure de type Union inexistante en Java. un événement HOJ est représenté par une classe Event ayant la structure générale suivante: public class Event { private BaseEventU BE\T ; private Eventu CHOOSE ; private String GUARD ; private char type; Lors de la création d'un événement. un seul des trois premiers champs est initialisé et les deux autres contiennent la valeur null. La variable type contient alors les caractères 'b', 'c' ou 'g' selon que le champs BEVT, CHOOSE ou GUARD ait été initialisé. La classe BaseEvent est une classe abstract qui contient des champs privés ainsi que les déclarations des méthodes que doit implanter un événement de base, soient: pollfn: qui consiste à vérifier si l'événement est prêt pour la synchronisation et qui retourne true si c'est le cas. dofn: qui contient les actions à exécuter dans le cas où pollfn retourne true. Plus précisément, Ia méthode dofn de la classe Sending appelle send et la méthode dofn de la classe Receiving appelle recv. blockfn: qui contient les actions à esécuter dans le cas ou pollfn retourne false. Pour chaque nouveau type d'événement de base, une sous-classe de BaseEvent définissant le comportement de l'événement est créée. Jusque la deux types d'événements de base ont été implantés: ceux dont la fonction consiste a envoyer un message sur un canal (classe Sending ), et ceux dont la fonction consiste à recevoir ces messages (classe Receiving ). La figure 5.8 illustre l'organisation des événements de base. Génération d'événements Le langage CML fournit un certain nombre de fonctions permettant de générer des événements. Plusieurs d'entre-elles ont été implantées au niveau du paquetage HOJ soient: les générateurs d'dvénements BEVT sendevt et recvevt et d'autres générateurs d'événements tels guard, wrap et choose.
: are ClockFn Figure 5.8: Événements de base. sendevt: cette méthode génère un événement BEVT qui est une liste contenant un événement de base de type Sending: BEVT [ev-de-type-sending]. C'est un envoi latent sur un canal. Sa synchronisation fait à l'appel de la méthode sync et un message est envoyé sur le canal spécifié par les paramètres. Le code suivant crée un événement dont la ~~ynchronisation envoie le message ml sur le canal chl: Event el=chl.sendevt(ml); recvevt: cette méthode génère un événement BEVT qui est une liste contenant un événement de base de type Receiving: BEVT[~V-de-type-Receiving]. C'est une réception latente sur un canal. Sa synchronisation fait a l'appel de la méthode recv et un message est envoyé sur le canal spécifié par les paramètres. Le code suivant crée un événement dont la synchronisation reçoit un message sur le canal chl: Event el=chl.recvevt(); guard: cette méthode génère un événement de type GUARD qui est une structure contenant une chaîne de caractères désignant le nom d'une méthode à exécuter lors de la synchronisation et un objet spécifiant l'emplacement de cette méthode. Le résultat d'une méthode guard retourne un événement qui est à son tour synchronisé. L'exemple qui suit montre comment appeler la méthode guard avec ses paramet res:
public class Exemple public Object methode(0bject argument) { Event el= chl.recv vt(); return el; 1 public static void main(string argsfl) C Exemple emplacement = new Exemple(); Event e2 new vent(); a wrap: cette méthode appliquée à un événement en génère un nouveau de même type que l'événement initial mais qui contient en plus des informations concernant une méthode a exécuter après la s_vnchronisation. Cette méthode prend en argument le résultat de la synchronisation de l'événement initial et lui applique un certain traitement. En CML, un événement wrap se comporte comme un événement de base en ce sens qu'il possède ses propres fonctions pollfn, dofn et blockfn. Sa fonction dofn est une composition de la fonction à exécuter. Supposons que cette fonction se nomme wrapfn, dofn peut s'écrire de la sorte: dofn = wrapfn O dofn Au niveau du paquetage HOJ, wrap ne génère pas d'événement de base à cause de la difficulté qu'entraînerait l'implantation de l'opérateur de composition o. Au lieu de cela, un test est fait lors de la s_vnchronisation pour vérifier si l'événement renferme des informations sur une méthode à exécuter pour un wrap. Dans ce cas, cette méthode est exécutée en prenant comme argument le résultat de dofn. choose: cette méthode permet d'effectuer un choix parmi une liste d'événements. Elle peut générer deux types d'événements: - BEVT: si tous les événements de la liste sont des BEVT. Par exemple, la méthode choose appliquée à la liste 11 retourne BEVT (bl,b2,b3): Evrntfl Il = new Ev~~~{BEw [~I].BEvT [~~],BEvT [b3]); Event ev2=choose(ll) ; - CHOOSE: si les événements de la liste sont de types hétérogènes. Par exemple, la méthode choose appliquée a la lise 12 retourne CHOOSE [GUARD,-
Eventu 12 = new EV~~~{BEVT [~~],GUARD.BEVT jb3j); Event ev2=choose(l2); Le choix en tant que tel se fait au moment de la synchronisation en fonction des priorités et des validités des événements de base. Synchronisation Synchroniser un événement revient à activer la communication latente qu'il renferme. La synchronisation des événements HOJ se fait par l'exécution de ia méthode sync. L'exécution de cette méthode se fait en plusieurs étapes: Étape 1: Exkution des Bvhements de type guard: La première étape lors de la synchronisation des événements est l'exécution des méthodes spécifiées par la chaîne de caractères GUARD. Ces méthodes retournent des événements et ce sont e u qui devront être véritablement synchronisés. L'exécution des GUARD se fait par l'appel de la méthode force. Lorsque cette méthode est appliquée à un événement, elle vérifie d'abord son type: - S'il s'agit d'un BEVT: la liste dfévénements de base contenue dans BEVT est retournée. - S'il s'agit d'un CHOOSE : il se peut alors que la liste d'événements contenue par le tableau CHOOSE contienne des GUARD. Un test récursif est alors appliqué sur chaque élément du tableau CHOOSE de façon a exécuter tout les GUARD. Une fois les GUARD exécutés, le tableau de CHOOSE ne contient plus que des CHOOSE [BEVT LBEVT 2...1 OU des BEVT. Il est alors aplati et une liste d'événements de base est retournée. - S'il s'agit d'un GUARD : la fonction est exécutée récursivement jusqu'à ce que son résultat ne contienne plus de GUARD. Comme pour le cas précédent, une liste de BEVT est retournée. Étape 2: Selection de IUvénement B exécuter: la méthode force retourne une liste d'événements de base. II faut ensuite appliquer une sélection à cette liste pour obtenir l'événement de base dont la fonction dofn sera exécutée. La méthode select est alors appelée: - Un premier tri des événements est d'abord effectué en appliquant la fonction pollfn respective à chacun d'eux. Les événements bloqués sont éliminés et une liste d'événements prêts à être synchronisés (pollf n à retourné true) est obtenue.
5.4 Ban CS d 'essais 78 - Un deuxième tri est alors effectué sur la liste d'événements obtenue. Ce tri est basé sur les priorités des BaseEvent et l'événement de base de priorité maximale est retourné en résultat. Si la priorité maximâle est partagée entre plusieurs événements de base, alors un choix aléatoire est appliqué à ces derniers. 0 Étape 3: Activation: La dernière étape consiste à activer l'événement de base sélectionné en lui appliquant la méthode dofn. Si L'événement de départ avait été créé à partir de la méthode wrap avec une fonction fn en argument.. le résultat de la synchronisation est alors passé en paramètres à fn et la valeur de retour de cette fonction constitue le résultat final. 5.4 Bancs d'essais Cet te section détaille trois problèmes typiques de synchronisation: L'exemple du Producteur/Consommateur; 0 L'exemple du dîner des philosophes; 0 L'exemple de l'ordonnanceur: Ces trois problèmes ont été résolus et codés avec et sans l'utilisation du paquetage HOJ de façon a permettre une meilleure évaluation de ce dernier. Ln consommateur récupère tous les objets créés par u=i ou plusieurs producteurs. Le problème est de s'assurer que le consommateur ne récupère pas deux fois le même objet et que le producteur ne remplace pas un objet avant que celui ci n'ait été récupéré par le consommateur. La table 5.1 donne le code en HOJ d'un problème de la sorte: Deuu producteurs Pl et P2 créent respectivement les entiers 1 et 2. Un consommateur C doit les récupérer dans le bon ordre et sans redondance. Pour cela un canal d'entiers ch est créé ainsi que des instances des HojThreads spécialisés Sender et Receiver. La mise en parallèle de Pl, P2 et C permet au consommateur de récupérer les entiers 1 et 2 envoyés par Pl et P2. Ceci est une implantation très simple du problème du producteur/consommateur. Elle illustre cependant la facilité d'utilisation du paquetage HOJ. 5.4.2 Dîner des philosophes Cinq philosophes qui n'ont rien d'autre à faire que penser et manger sont assis autour d'une table ronde. Entre chaque philosophe, il ÿ a une fourchette. Sachant que chaque
5.4 Bancs d'essais 79 import java.io.*: import hoj.*: public class HojCubby public static void main(string argso) C Chan-of-lnteger ch = new Chan-of-lnteger(); Sender Pl = new Sender(ch.1): Sender P2 = new Sender(ch.2); Receiver C= new Receiver(ch); Pl.spawn(); P2.spawn(): Cspawn (); Tableau 5.1: Codage du Producteur jconsommateur en HO J. philosophe a besoin de deu fourchettes pour manger, le problème réside en la capacité qu'auront les philosophes à se partager les fourchettes pour que chacun mange a sa faim et ce, sans occasionner d'interblocages. La solution que nous avons implantée est peut être résumée par les équations de la table 5.2 où Pl... P5 sont des processus représentant les philosophes. FI... F5des processus représentant les fourchettes et ch1... ch5 des canaux de communication. Les deus actions Penser et Manger des philosophes sont en fait des temps d'attente. Chaque philosophe i commence par penser. Après quoi il demande sa fourchette de droite et sa fourchette de gauche en envoyant un message sur les canaux i et i - 1 (1 et 5 pour i = 1). Il mange, dépose les fourchettes en envoyant de nouveau des message sur Ies canaux i et i - L et se remet à penser. Pour chaque fourchette i s'effecutent deus réceptions consécutives sur le canal i. Sous adoptons la convention qui veut que ch corresponde à un envoi sur le canal ch et ch à une réception sur ce canal. La mise en parallèle des processus Pi et Fi exécute les actions suivantes: au départ les 5 philosophes pensent. Ils envoient ensuite des messages sur les canaux correspondant à leur numéro pour demander la fourchette de droite. Les processus Fi reçoivent parallèlement ces messages et attendent l'arrivée des suivants sur les mêmes canaux. Une fois les premiers messages reçus, les philosphes demandent les fourchettes de gauche ce qui a pour effet de débloquer les processus Fi en attente. Les philosophes peuvent alors manger. Il déposent ensuite les fourchettes et recommencent à penser. Losque des fouchettes sont occupées ailleurs, les philosophes
5.4 Bancs d'essais 80 Tableau 5.2: Équations des Philosophes. attendent jusqu'à ce qu'elles soient disponibles. Les canaux de type rendez-vous gèrent automatiquement les attentes des processus. 5.4.3 Ordonnanceur Ln ordonnanceur est requis pour gérer l'ordre exécution de cinq agents Pl, P2,... P5 [SIi189]. Les agents effectuent chacun une tâche de manière répétitive et loordonnaceur doit s'assurer qu'ils sont exécutés de façon cyclique en commençant par Pl. L'esécution des tâches n'est pas exclusive c'est à dire que P2 peut commencer avant que Pl n'ait terminé. Cependant, un agent ne peut commencer une autre tâche avant d'avoir terminé la précédente. Supposons qu'une tâche est constituée de deux actions: a pour le départ et b pour Ia fin. Les actions ai doivent être effectuées dans l'ordre en commençant par al. Pour tout i les actions bi ne peuvent pas s'effectuer avant les actions ai. Un ordonnanceur imposant une séquence fke telle que alblasb2... n'est pas considéré comme étant acceptable car il n'est pas assez flexible. Nous avons adapté la solution proposée par Milner a notre implantation par canaux comme Ie montre la table 5.3. Soient S1... S5 et Pl... P5 des HojThreads et ai, bi et ci des canaux de type rendez-vous. - En adoptant la convention qui veut que ch corresponde à un envoi sur le canal ch et ch à une réception sur ce canal, la mise en parallèle des processus Pi et Si exécute les actions dans le bon ordre en respectant la spécification. Les canaux ai sont des indicateurs de début de tâche, les canaux bi des indicateurs de fin de tâche et les canaux ci des indicateurs de tâche en exécution. L'exécution des Ai est conditionnée par les ci - 1 sauf -41 qui est le premier système à s'exécuter. Ceci
5.4 Bancs d'essais 81 - - -- Figure 5.9: Dîner des philosophes. oblige les Pi à s'exécuter dans l'ordre. Consiérons le processus Al. Une première synchronisation avec Pl débute la tâche. Cne synchronisation avec S confirme l'exécution de la tâche et permet à A2 de commencer. -Arrive alors un choix entre terminer la tâche courante puis confirmer l'exécution de la tâche précedente (permet à Pl de terminer avant P5) ou de confirmer la l'exécution de la tâche précédente puis de terminer la tâche courante (permet à P5 de terminer avant Pl). Le choix entre des séquences de communications est obtennu en HOJ grâce aux événements en combinant les fonctions wrap et choose. 5.4.4 Résultats Le codage et l'exécution des exemples du producteur/consommateur, du dîner des philosphes et de l'ordonnanceur permettent d'évaluer notre paquetage. La table 5.4.4 montre le nombre de lignes, la memoire ainsi que le temps requis pour coder ces exemples avec et sans le paquetage HOJ. Lorsque les applications impliquent un transfert quelconque de données entre différents threads, la programmation est beaucoup plus rapide avec le paquetage HOJ comme le montre I'exemple du producteurjconsommateur. En effet, l'application Java nécessite, en plus des threads, la création d'un ou de plusieurs objets tampons supplémentaires déstinés à faire transiter les données, à synchroniser les threads et à gérer l'ordonnancement, ce qui est géré implicitement par les canaux HOJ.
5.4 Ban CS d 'essais 82 Tableau 5.3: Équations de I'ordonnanceur Producteur/Consommateur Java HOJ Dîner des Philosophes Java HOJ Ordonnanceur HOJ Lignes 71 ln 16 In Lignes 93 In 109 ln Lignes 127 ln Temps 46ms 58ms Temps 7560ms 4716ms Temps 1752ms Mémoire 120288 120952 Mémoire 134016 136648 Mémoire 147672 Tableau 5.4: Résultats Cependant certaines applications nécessitent uniquement des mécanismes d'éxclusion mutuehe binaire. Dans ce cas, l'utilisation du paquetage HOJ ne réduit pas toujours la taille du code comme le montre l'exemple des philosophes. Nous gagnons cependant en temps d'esécution. L'exemple de l'ordonnanceur est particulièrement approprié pour illustrer l'utililité des événements. Nous ne l'avons pas codé en Java à cause de la complexité qu'entraîne le chois sur des séquences de communications. Cet exemple intègre plusieurs problèmes de communication et de synchronisation et a constitué un bon test pour notre paquetage. Il nous à permis de déceler certains défauts d'implantation et de les corriger. Le code source des bancs d'essais au complet peut être consulté à l'annexe A.
5.5 Conclusion 83 5.5 Conclusion Ce chapitre détaille l'architecture et l'implantation du paquetage HOJ. Ce paquetage comporte trois composantes principales soient: les threads, les canaux et les événements. Mis à part les threads qui sont ceux de Java augmentés de quelques nouvelles méthodes, les canaux et les événements ont été entièrement basés sur ceux du langage CML. Les canaux HOJ intègrent les structures Mailbox et syncvar de CML à travers des unités de stockages ou DataStores. Le langage Java n'étant pas générique, ce sont des canaux "semi typés" en ce sens qu'il existe un canal spécifique pour tous Ies types primitifs de Java alors que les types de référence utilisent un canal commun d'objets. Les canaux HOJ vérifient la propriété de mobilité des canaux CML: il est toujours possible d'envoyer un canal sur un canal. Les principales fonctions des événements ChIL ont également été implantées. Des programmes d'essai codés avec et sans l'utilisation du paquetage HOJ ont permis de \-érifier sa pertinence ainsi que son efficacité.
Chapitre 6 Conclusion L'objectif principal de ce projet était de proposer un nouveau paradigme de programmation qui améliorerait la composante parallèle actuelle du langage Java en proposant des constructions syntaxiques qui abstraient la communication. la synchronisation et l'ordonnancement. Après une brève introduction des langages parallèles et de leurs mécanismes de communication et de synchronisation, nous avons procédé à une étude approfondie des composantes parallèles au niveau des langages Java et Concurrent -ML. Le langage CML s'étant révélé un excellent langage de programmation parallèle, nous avons implanté ses principales primitives de parallélisme au niveau de Java en proposant un nouveau paquetage parallèle: HOJ ou Higher Order Java qui se veut une version allégée du concept de "Higher Order Concurrency" introduit par John Reppy- le paquetage HOJ intègre donc trois primitives de parallélisme: les threads: les canaux; les événements: Les threads HOJ sont en fait des threads Java auxquels nous avons ajouté cert.aines fonctionnalités. Comme les threads CML, ils sont exécutés à partir le la méthode spawn qui retourne un idendificateur de thread thread-id. Les canaux et les événements constituent les éléments réellement nouveaux apportés par HO J. L'absence de polymorphisme en Java a constitué un problème majeur pour l'implântation de ces éléments. En effet, il nous a été impossible de fournir des canaux et des événements typés comme en CML. Nous avons quelque peu contourné le problème en fournissant des canaux "semi-typés". En CML, il existe un type de canal correspondant à chaque type de donnée envoyée ou reçue. Au niveau du paquetage HOJ, ceci n'est vrai que pour les données de type primitif, les autres données se partageant un canal
collectif d'objets. Sous avons adopté une version simplifiée des événements CML que nous avons représenté comme une union de 3 types de données au lieu de quatre. Soient BEVT. GUARDet CHOOSE. Toutefois, les classes constituant les événements ont été organisées de sorte que l'ajout de nouvelles compostantes se fasse très simplement sans ajout de code redondant. Le paquetage HOJ peut s'avérer d'une grande utilité pour la programmation parallèle en Java en y ajoutant les concepts de canaux et d'événements. Nous n'avons considéré pour son implantation que les primitives principales offertes par CML. Il serait donc intéressant de le compléter en y ajoutant le reste des fonctionnalités de ce langage. De plus, seuls les aspects de communication, de synchronisation d'ordonnancement et d'abstractions ont été traités au niveau de ce paquetage. Des travaux futurs visant à corriger les failles au niveau de la portabilité de la sémantique formelle et de la performance du paquetage de threads de Java permettraient de faire de ce langage un outil idéal pour la programmation parallèle.
Bibliographie Deb941 Dem] [Demg81 [Dij 751 Burns Alan. Concurrent Programming. -Addisson-Wesley pub. Co 1993. G-Padiou -4.Sayah. Techniques de synchronisation pour les applications parallèles. 1990. P. Austin. Java communicating sequential processes 'design of jcsp laquage classes'. Daniel J. Berg. Java Threads, A Whitepaper. Sun Microsystems Cornputer Corporation, 2550 Garcia Avenue Mountain View. CA 94043. US-4. March 1996. CROCUS. Systèmes d'exploitation des ordinateurs, Principes de conception. DUNOD, 1975. Mary Campione and Kathy Walrath. The Jaua Tuton'al Second Edition: Object-Oriented Programming for the Internet. The Java Series. Addison- Wesley Reading, MA, second edition, 1998. Mourad Debbabi. Intégration des Paradigmes de Programmation Parallèle, Fonctionelle et Impé~ative: Fondements Sémantiques. PhD thesis, Cniversité Paris-Sud. U.F.R. Scientifique d'orsay, 1994. E. D. Demaine. Higher-order concurrency in java. Dept. of Cornputer Science, University of Waterloo, Waterloo, Ontario, Canada K2L 3G1. Erik D. Demaine. Higher-order concurrency in java, May 19 1998. E. W. Dijkstra. Guarded commands, non-determinacy and a calculus for the derivation of programs. A CM SIGPLAN Notices, 10 (6): 2-14, June 1975. James Gosling, Bill Joy? and Guy L. Steele. The Java Language Specijication. The Java Series. Addison- Wesley, Reading, MA, USA, 1996. IHBVB97J Gerald Hilderink, Jan Broenink, Wiek Vervoort, and Andre Bakkers. Communicating Java Threads. In A. Bakkers, editor, Parallel Prograrnming and Jaua, Proceedings of Wo TUG 20, volume 50 of Concurrent Systems Engineering, pages 48-76, University of fiente, Netherlands, April 1997. World Occam and Transputer User Group (WoTUG), IOS Press, Netherlands.
C. -4. R. Hoare. Monitors: An operating system structuring concept. Communications of the ACM. lï(10): 544-557, October 1974. Erratum in Communications of the ACM. Vol. 18, No. 2 (February), p. 95, 1975. This paper contains one of the first solutions to the Dining Philosophers problem. C. -4. R. Hoare. Communicating sequential processes. Communications of the ACM. 21(8): 666-677, August 1978. See conïgendum [Hoa78]. C. -4. R. Hoare. Cornmunicating Sequential Processes. Prent ice-hall. 1984. Alan JefTrey. Book Review: ML with Concurrency by Flemming Nielson (ed.). Springer-Verlag, 1997. Journal of Functional Programming. 8(5) : 537-542, Septernber 1998. R. Milner. Communication and Concurrency. Prentice Hall international series in cornputer science, 1989. Prakash Panangaden and John H. Reppy. The essence of Concurrent ML. In Flemming Nielson. editor, ML vith Concurrency, chapter 1. Springer- Verlag, 1997. John Reppy. Concurrent Programming with Events. Corne1 November 1990. the Concurrent ML Manual (Version 0.9). John H. Reppy. Higher-order Concurrency. PhD thesis, Department of Computer Science, Corne11 University. January 1992. John H. Reppy. Higher-order concurrency. Technical Report TR92-1285, Corne11 University, Computer Science Department, June 1992.
Annexe A Code source des bancs d'essais
-4.1 Producteiu/Consommateur en Java 89 A. 1 Producteur/Consommateur en Java public class Consumer entends Thread i private CubbyHole cubbyhole; private int number; public Consumer(CubbyHo1e c, int number) i cubbyhole = c; this. number = number ; 1 public void nia0 C int value = 0; value = cubbyhole. get 0 ; System. out. println("cansumer #" + this. number + " got : " + value) ; 3 3 public class Producer extends Thread C private CubbyHole cubbyhole; private int number; public Producer(CubbyHo1e c, int number) C cubbyhole = c; this. number = number ; 1 public void run() ( int i=l; cubbyhole.put (i) ; System.out.println("Producer #" + this-number + " put: " + i); public class CubbyHole C private int contents ; private boolean available = false; private Object put-monitor = neu ObjectO; private Object get-monitor = nev ObjectO; public int get 0 < vhile (available == false) C uait () ; ) catch (InterruptedException e) ( >
-4.3 Prod ucteur/consommateur en HOJ 90 > 1 available = false; notifyall(1 ; return contents ; public void put(int value) uhile (available == true) C try i wait 0 ; catch (InterruptedException e) C ) 3 contents = value; available = true; not if yall() ; public class ProducerConsumerTest C public static void main(~tringa args) C CubbyHole c = nev ~ubbyhole() ; Producer pl = new ~roducer Cc. 1) ; Consumer cl = nev consumer (c, 1) ; pl. start O ; cl. start O ; import java.io.*; import hoj.*; public class HojCubby i public static void main(string argsu)
-4-3 Dîner des philosophes en Java 91 C Ob j ectstore os = neu ~ingleobject (1; Chan-of,Ob ject ch = nev Chan-of,Ob ject 0 ; Sender s 1 = new Sender (ch, 1) ; Receiver rl= neu Receiver(ch1; sl. spawn0 ; rl- spaun0 ; -4.3 Dîner des philosophes en Java public class JPhils extends Thread int number ; JForks right; JForks left; public JPhils (JForks r, JForks 1, int i) < right=r ; left=l; number = i; > public void runo //vhile (true) for ( int i=o; i<2; i++) Systern.out.println("Philosopher "+number+" thinks"); tryc sleep (1000);)catch(InterruptedException e) 0 System.out.println("Philosopher "+nr;mber+" is hungry nov"); System.out.println("Philosopher "+nuber+" asks right fork"); right. getfork 0 ; System.~ut.println(~Pbilosopher "+number+" asks left fork"); lef t. getfork O ; Systern.out.println("Philosopher "+nuber+" eats"); try( sleep (1000) ; )catch (InterruptedException el<) System.out.println("Philosopher "+number+" releases right fork"); right.releasefork0; System.out.println(~Philosopher "+nuber+" releases left fork"); lef t. releasefork O ; 1
-4.3 Dîner des philosophes en Java 92 > public class JForks boolean taken = false; int number ; public synchronized void getfork0 < while (taken) i try(uait (1 ; ) public synchronized void releasefork0 C taken= false; not if yall() ; public class JPhilstest < public static void main (String argsn) C JForks f 1 = nev JForks () ; JForks f2 = nev JForksO; JForks f 3 = new JForksO ; JForks f 4 = new JForks O ; JForks f5 = nev JForksO; JPhils pl = nev JPhils(f1,fS.l); JPhils p2 = neu ~Phils(f2.f l,2) ; JPhilç p3. = nev JPhils (f3, f 2.3) ; JPhils p4 = new JPhils(f4,f3,4); JPhils p5 = new JPhils(fS.f4,5); pl. start O ; p2. start O ; p3. start O ; p4.starto ; p5. start O ;
A. 4 Dîner des philosophes en HOJ 93 A.4 Dîner des philosophes en HO J public class Phils extends HojThread private Chan-of-Object cl; private Chan-of-Object c2; private Chan-of-Object c3; private Chan-of-Object c4; pr ivat e Chan-of,Ob j ect CS ; private Chan-of-Object right-chamel; private Chan-of-Object left-channel; private int number; public Phils(Chan,of-Object chl, Chan-of-Object ch2, int i) public void runo for (int i=o; I<2; i++) System.out.println("Philosopher "+number+" thinks"); tryc sleep(1000);~catch(1nternipted~xception el{> System. out. println("phi1osopher "+nuber+" is hungry nou") ; System.out.println("Philosopher "+nuber+" asks right fork"); right-channel.send(1); Sgstem.out.printla("Pbilosopher "+number+" asks left forkw); left-channel.send(1) ; System.out.println("Philosopher "+number+" eats"); tryc sleep (1000) ; >catch (InterruptedException e) O System.out.println("Philosopher "+number+" releases right fork"); right-channel.send(2); System.out.println("Phi1osopher lt+number+" releases left forkv); left-channel.send(2);
-4.4 Dîner des philosophes en HOJ 94 public class Forks extends Hojihread C private Chan-of,Ob j ect right-channel ; private int number; public Forks (Chan-of,Obj ect ch, int i) C right,channel=ch; number=i ; public void runo C > right-charnel.recvo ; // System.out.printlnCWf ork is given") ; right-channel. recvo ; // System. out.println("f ork is released") ; public class Philstest public static void main(string args U) i Chan-of-Object ch1 = new Chan-of -0bject ("chl") ; Chan-of-Object ch2 = neu Chan,of,Object("chS"); Chan-of-Object ch3 = nev Chan-of-Object("ch3"); Chan-of,abject ch4 = neu Chan-of -0bject ("ch4") ; Chan-of-Object ch5 = neu Chan,of,Object("chS"); Phils pl = neu Phils(ch1,chS. 1) ; Phils p2 = neu phils (chs. chl.2) ; Phils p3 = neu Phils(ch3,ch2.3); Phils p4 = nev Phils(ch4,ch3,4); Phils p5 = new Phils (chs. ch4.5) ; Forks f 1 = neu Forks (ch1, 1) ; Forks f 2 = nev ~orks (CU, 2) ; Forks f3 = new Forks(ch3.3) ; Forks f4 = new ~orks(ch4,4) ;
-4.5 Ordonnanceur en HOJ 95 Forks f5 = new Forks(ch5,S); A.5 Ordonnanceur en HOJ import ho j. ; public class Task extends HojThread < private Chan-of-Object start; private Chan-of-Object done; private int number; public Task(Chan,of,Object st,chan,of,object don, int i) < start=st ; done=don ; number = i; > public void run0 C vhile (true) < start.recvo ; System. out.println(" Task "+ number+" starts (recv)") ; done.send(1) ; System.out.println(" Task "+number+"is done (send)"); public class Sched extends HojThread
-4.5 Ordonnanceur en HOJ 96 i private Chan-of,Obj ect start ; private Chan-of-Object started; private Chan-of,Ob ject startedprev ; private Chan-of-Object done; private int number; public Sched() C3 public Sched (Chan-of -0bject st, Chan-of -0bject sd, Chan-of,Ob j ect d, Chan-of,Ob j ect sp, int i) staxt=st; started=sd; startedprev=sp; done=d ; number = i; public void f l(0bject O) startedprev. recv() ; System.out.println("Ye>re in fl: startedprev receives"); 1 public voia fs(0bject O) System.out.println("Ye're in f2: startedprev receives"); 1 public void a0 i Event e= neu Event (); Sched 01 = new SchedO; Event edone=done. recvevt (); Event estartedprev = startedprev.recvevt ; edone = ed~ne.urapt"fl~~,ol) ; estartedprev = estartedprev. urap("f 2", 01) ; //vhile (true) for (int i =O; i< 2; i++) C Event el = neu Event(); System.out.println(" try start.send "+number); start. send(1) ; System.out.println(~~start.send ok "+number); System. out.println("try started. send "+number) ; started. send(1) ; System. out.printin("started.send ok "+nuber) ; System.out.printin("ci sended");
-4.5 Ordonnanceur en HOJ 91 el=e.choose(edone,estartedprev); el. sync 0 ; public void run0 I 1 else 1 //vhile (tme) I startedprev.recv0 ; System. out. println(i1startedprev. recv ok "+number) ; a0 ; > public class SchedTest C public static void main(string argsa) < Chan-of,Ob ject startl = neu Chan-of,Ob ject ("chl") ; Chan-of,Ob j ect start2 = new Chan-of,Ob ject ("CU") ; Chan-of-Object start3 = neu Chan,of,Object("ch3"); Chan-of-Object start4 = nev Chan,of_Object("ch4"); Chan-of,Ob ject start5 = new Chan-of,Ob ject ("ch5") ; Chan-of-Object donel = new Chan,of,Object("cl"); Chan-of-Object done2 = new Chan-of-Object("c2"); Chan-of,Ob ject done3 = nev Chan-of -0bject ("~3"); Chan-of -Ob j ect don04 = new Chan-of,Ob j ect ("~4"); Chan-of-Object done5 = new Chan-of-Object("c5"); Chan-of -Ob ject startedl = nev Chan-of,abject ("hl") ; Chan-of-Object started2 = new Chan-of-Object("h2"); Chan-of-Object started3 = nev Chan,of,Object("h3"); Chan-of -Ob ject started4 = nev Chan-of,Ob ject ("h4") ; Chan-of-Object started5 = neu Chan-of-Object("h5") ; Task pl = neu ~ask(startl,donel,l); Task p2 = nev Task(start2,done2,2); Task p3 = neu ~ask(start3,done3,3); Task p4 = neu ~ ask (start4, done4,4) ;
-4.5 Ordonnanceur en HOJ 98 > Task p5 = nev ~ask(start5,done5,5); Sched SI = neu Sched(startl,startedl,donel,started5,1); Sched 52 = nev Sched(start2, starteds.done2, startedl,2) ; Sched S3 = neu Sched(start3,started3,done3,started2,3) ; Sched S4 = nev Sched(start4,started4,done4,started3,4); Sched S5 = neu Sched(startS,started5,done5,started4,5); Sl. spaun0 ; S2. spavn0 ; S3. spaun0 ; ~ 4 spavn0. ; SS. spavn0 ; pl. spaun0 ; p2.spanio ; p3. spavn0 ; p4.spauno ; p5. spaun O ;