Lif10 Systèmes Distribués em Java
Transcription
Lif10 Systèmes Distribués em Java
T.D. 2 MIAGE Sophia Lif10 Systèmes Distribués em Java TD2 Création de Threads, Problèmes de Cohérence et de Synchronisation Toujours à faire au cas mais pas durant le TD! Installer la JDK Java Devellopment Kit et non pas seulement la JDE. Installer Telnet. Sous Windows entrer dans une invite de commande administrateur : dism /online /Enable-Feature /FeatureName:TelnetClient . Voir aussi ici comment récupérer Telnet pour d'autre méthodes. Vérier les chemins (en particulier sous Windows) , diverses méthodes sont données ici. Vous devriez trouver à la fois le compilateur java (javac) et la machine virtuelle (java). Pour exécuter la classe Classe_de_Test en ligne de commande il faut (en théorie) se placer à la racine de votre répertoire de travail Eclipse et taper javac nom_du_package:Classe_de_Test, puis idem pour java. Si votre paquet se nomme miage2016:td1 java va en eet chercher toutes les classes dans ./ miage2016/ td1. contact : Stéphane Perennes [email protected] Documentation : Le site d'Oracle fournit des : tutoriaux ainsi qu'une documentation ocielle. Les classes sont documentées ici. Pour ce jour (et même de façon générale), le blog Java Le Soir est me semble t'il assez indiqué Objectifs : - Comprendre les problèmes posés par concurrence. - Savoir lancer des Threads en Java. - Comprendre les principes des solutions proposées par les systèmes de gestion d'applications concurrentes (la JVM, un Système d'exploitation etc). - Savoir comment s'assurer l'exclusivité sur un objet en Java, et comment synchroniser 2 Threads (cas simples). Blabla ...zzzzzzzzzz Pourquoi la programmation distribuée et (où?) concurrente ? - Dans le cas des applications réseaux elle est en quelque sorte inhérente, en eet les infrastructures numériques actuelles répliquent l'information et gèrent des bases de données distribuées à l'échelle mondiale. - Une solution distribuée passe à l'échelle plus facilement qu'une application centralisée. On doit cependant noter que à cause des économies d'échelle on observe un certain retour à la centralisation (Data Center immenses), mais ces solutions sont toutes massivement distribuées bien que souvent localisées sur un site géographique restreint et disposant un réseau dédié et opérant des milliers de processeurs. - Une solution concurrente permet d'utiliser les nombreux coeurs des processeurs modernes. Le modèle formel dit datant des années 80 envisageait des processeurs partageant une certaine quantité de mémoire vive; il est aujourd'hui devenu une réalité car on trouve aujourd'hui des processeurs très abordables ayant 64 coeurs, et l'ordinateur de base en contient 4 à 8. pram - Une application distribuée est aussi en théorie plus able car une application centralisée est vulnérable en son coeur. Cependant le comportement d'un processus distribué est bien plus complexe que celui d'un processus centralisé. Mettre au point une application distribuée qui fonctionne correctement en absence de malice est déjà dicile, mais la sécuriser est extrêmement dicile Syst. Distribués. Page 1/6 October 14, 2016 T.D. 2 MIAGE Sophia - La programmation distribuée est aussi un paradigme qui permet de concevoir plus facilement une application, car il est souvent facile de l'imaginer comme un ensemble de tâches à eectuer. Par exemple un serveur de chier peut être un considéré comme un ensemble de tâches (coordonnées) servant un chier donné à un client donné. De même les applications proposant une interface graphique utilisent souvent "tâche interface" qui communique avec le reste de l'application. Ou encore, un navigateur internet peut utiliser un thread par page, ou par contenu. En quelque sorte la programmation distribuée impose ou du moins conduit à programmer de façon modulaire, et c'est ce qui est attendu de la part d'un développeur. Systèmes avec ou sans arbitre ? La JVM gère, ordonnance les threads et permet de gérer la cohérence des données, c'est aussi ce que fait le noyau d'un système d'exploitation. Dans les deux cas le calcul distribué s'eectue sous l'égide d'un arbitre able et que l'on suppose contrôler parfaitement. Le cas d'un système distribué (sur le réseau) doté d'un coordinateur central est en principe assez analogue (bien que la latence des opérations soit plus élevée). Par contre, nous ne considérons pas ici et maintenant les systèmes de type pair à pair sans arbitre, dans de tels systèmes procurer une fonctionnalité aussi élémentaire qu'une variable globale est délicat (quels participants vont stocker x ? mentent t-il à propos de cette valeur ? sont-ils en panne? aucun participant n'est able...). La seule facon de procéder consiste à demander la valeur de x à de nombreux pairs choisis au hasard et à se er à la réponse majoritaire. La théorie démontre même dans le cas de pairs malicieux et byzantins (i.e. cherchant à rendre le système défectueux) qui exécutent le code qui leur chante on peut cependant encore émuler un système avec arbitre able, sous la condition qu'au moins 50% des pairs jouent parfaitement le jeu. Mais c'est très complexe et cela induit un surcoût important pour la couche de contrôle. Donc on ne considère pas ce cas ici 1 La création de threads sous Java Au minimum il nous faut savoir créer des Threads. Notons que les Threads disposent de leur champs propres mais partagent les objets extérieurs sur lesquels ils pointent, parmi ces objets ont trouve bien entendu les objets globaux. Il est donc assez facile de partager de la mémoire (sic des objets) tout en ayant une mémoire locale propre au thread. Mais ce partage à un prix, car qui dit objet partagé dit possibilité d'eectuer en même temps des opérations dessus ce qui aura souvent pour eet de faire planter. On peut avoir une idée des problèmes posés en utilisant Google Doc et en éditant à deux le même document, Si deux utilisateurs corrigent au même moment à peu près au même endroit le résultat sera en général incohérent. En Java, la façon la plus simple pour créer un thread est la suivante : - On dénit une classe qui implémente l'interface Runnable public Classe_de Test implements Runnable { - On redéni la méthode run() public void run() ..... de la classe : {........ } Attention on ne peut pas passer de paramètres à la méthode run(), si donc l'objet doit pointer sur des objets externes on doit le faire lors de la création, ou avec une méthode, avant de créer le thread proprement dit. - On crée l'objet exécutable : Classe_de_Test mon_objet_executable = Classe_de_Test(); - Enn on crée le Thread lui même, puis on le démarre : Thread monJob = New Thread(mon_objet_executable); monJob.start() Attention ! ici encore, si start() exécute bel et bien run() elle eectue bien d'autres opérations (cachées). En eet elle demande au gestionnaire de Thread d'en créer un nouveau, de le référencer , de lui allouer de la mémoire, de l'ordonnancer et de l'exécuter. Ainsi par exemple l'objet global Threads contiendra dès lors de nombreuse informations aérentes au thread. Au nal cela donne : 1 public 3 5 7 9 c l a s s DumbClass implements S t r i n g nom=" Toto " ; i n t maxv= 1 0 0 ; p u b l i c DumbClass ( S t r i n g nom ) t h i s . nom=nom ; } p u b l i c v o i d run ( ) { System . o u t . f o r m a t ( " I c i Syst. Distribués. Page 2/6 le Runnable { { t h r e a d %s , je d eb u te ! \ n" , October 14, 2016 nom ) ; T.D. 2 MIAGE Sophia 11 for 13 } 15 ( i n t i = 0 ; i < maxv ; i ++) { System . o u t . f o r m a t ( " [% s ] d i t je suis l a %d \ n " , nom , i ) ; p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) throws S t r i n g jobname= S t r i n g . f o r m a t ( "Job_%d " , 0 ) ; DumbClass objet = new DumbClass ( jobname ) ; 17 Thread j o b = new Thread ( o b j e t ) ; System . o u t . f o r m a t ( " C r e a t i n g t h r e a d %s \ n " , job . s t a r t () ; System . o u t . f o r m a t ( " Main : F i n i i c i ! \ n" ) ; } 19 21 23 } Exception { jobname ) ; } ../DumbClass.java Question 1 Écrire un programme qui crée deux Threads TA et TB, l'un devra compter de 1 à 1000 et l'autre décompter de 1000 à 1. Il y a une autre facon de créer un Thread, on peut simplement hériter de la classe Thread. L'objet est alors directement un Thread et on appelle la méthode start sur l'objet lui-même sans créer de Thread car l'objet en est un. 1 public 3 5 7 9 11 13 15 17 c l a s s DummyClass e x t e n d s Thread S t r i n g nom=" Toto " ; i n t maxv= 1 0 ; p u b l i c DummyClass ( S t r i n g nom ) { t h i s . nom=nom ; } { p u b l i c v o i d run ( ) { System . o u t . f o r m a t ( " I c i l e t h r e a d %s , j e f o r ( i n t i = 0 ; i < maxv ; i ++) { System . o u t . f o r m a t ( " [% s ] d i t je suis d eb u te ! \ n" , nom ) ; l a %d \ n " , nom , i ) ; }} p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) throws Exception S t r i n g jobname= S t r i n g . f o r m a t ( "Job_%d " , 0 ) ; DummyClass objet_executable = new DummyClass ( jobname ) ; System . o u t . f o r m a t ( " C r e a t i n g t h r e a d %s \ n " , jobname ) ; objet_executable . start () ; System . o u t . f o r m a t ( " Main : F i n i } ici { ! \ n" ) ; 19 } ../DummyClass.java 2 Quelques exemples de concurrence et de coordination en Java Un exemple sans réelle concurrence, enn ... un peu mais pas méchante ... Question 2 Attention ce programme utilise deux arguments : i.e if (args.length != 2) { System.exit(-1); System.err.format("utilisation java %s nbthreads maxvalue ", Test_ordre Sous Eclipse les arguments sont dénis via le memu Run->Run Conguration puis sélectionner l'onglet paramètres. Lire, télécharger - ou recopier - le programme TestOrdre0.java, l'exécuter plusieur fois. Que fait le programme, que fait chaque thread ? L'achage sur la sortie standard est il déterministe ? ou le semble t'il seulement ? Ajouter dans la fonction run() des délais aléatoires (représentant l'activité à priori inconnue de threads plus complexes) an de rendre le comportement encore plus chaotique. Quel est la principale ressource utilisée concurremment par les threads ? Syst. Distribués. Page 3/6 October 14, 2016 T.D. 2 Figure 1: MIAGE Sophia Inconsistance (dite de race-condition), ici bien que les fonctions f : x → x+1 et g : x → x+1 commutent le calcul retourne n'importe quoi. Exemple canonique d'inconsistance grave Lire, télécharger - ou recopier - le programme Petit_job.java, (il utilise IntHolder.java). Éxécuter Pe- tit_Job. Que fait le programme, que fait chaque thread ? Quelle devrait être la valeur stockée par l'ObjetEntier nommé Compteur à la n du calcul ? Expliquer ce qui arrive, ou se trouve la concurrence ? Comment pourrait-on faire pour que le calcul retourne 0 et cela de façon certaine ? Consistance des données et problèmes ordonnancement L'absence de l de calcul unique et déterministe pose de nombreux problèmes. Un cas simple (d'ordonnancement) et de consistance est le suivant : - le thread A lit x calcule f (x) - le thread B lit x calcule quand à lui et remplace g(x) x par f (x). et remplace x par g(x). Le résultat est complètement imprévisible, à la n du calcul on peut en eet trouver aussi bien f (x) ou g(x). f (g(x)), g(f (x)) mais tout L'exemple classique est donné dans la gure 1. Pour résoudre les problèmes de consistance des données, les systèmes distribués fournissent divers mécanismes, leur but est le même s'assurer l'exclusivité de l'accès à des parties communes. - (UNIX, Noyau Système): On peut déclarer des sections critiques (de code) et utiliser des opérations atomiques. Quand une tâche eectue une section critique le système assure qu'elle ne sera pas interrompue; les tâches qui sont ainsi bloquées sont mises en sommeil et réveillées quand la phase critique est terminée. - (UNIX,Java) Le sémaphore : Celui ci permet de réserver une ressource ou un objet, quand l'autorisation est délivrée (en attendant la tâche sommeille) le système assure que personne d'autre n'accède à l'objet ou la ressource. Quand la tâche à terminé son travail critique elle informe le système qui va alors délivrer une autre autorisation et potentiellement réveiller une des tâche en attente (si il y en a). - En Java on peut verrouiller un objet un utilisant la déclaration synchronized (c.f ci-dessous), là encore le système distribué de la JVM va endormir les Threads (quand la ressource est occupée) et elle réveillera ces Thread en attente quand la ressource sera libérée. Le sémaphore dière un peu du verrou, en eet c'est le sémaphore qui techniquement sait si la ressource critique est occupée ou disponible alors que dans le cas d'un verrou on utilise un jeton et celui qui souhaite utiliser la ressource s'empare du jeton et le rend quand il en a terminé. Pour gérer les problèmes d'ordonnancement les systèmes distribués permettent aussi : - de terminer une tâche - d'attendre qu'une tâche soit terminée. - de mettre en sommeil ou de réveiller une tâche. Syst. Distribués. Page 4/6 October 14, 2016 T.D. 2 MIAGE Sophia Consistance et Verrouillage, le cas de Java Une des directive de verrouillage en Java consiste à déclarer qu'une méthode ou un bloc de code est synchronisé. Le Verrouillage de méthode public class Compteur_Synchro { private int c = 0; .......blabla............... public synchronized void add(int j) { c+=j; } Ici la méthode add(int j) est déclarée synchrone, cette déclaration empêche que plus d'un thread exécute une méthode synchrone de l'objet concerné. Les méthodes non synchrones de la classe ne sont pas aectées et bien entendu les appels eectués sur des objets diérents de la classe Compteur_Synchro ne sont pas aectés. Si toutes les méthode de la classes sont synchrones, l'objet est complètement verrouillé. C'est exactement le genre de verrou qu'il nous faut dans le cas du compteur de l'exemple Petit_job.java, Question 3 Dans l'exemple Petit_job.java, - Quelle méthode pose problème ? La rendre synchrone. - Observer le résultat ? Pourquoi le thread principal n'ache t'il pas 0? 9 - Ajouter une longue boucle (10 tours) juste avant la n di programme principal et acher à nouveau le compteur, que concluez vous ? Attention ! Si on verrouille une méthode statique on va verrouiller tous les objets de la Classe, ce qui parfois peût être utile. Le Verrouillage de bloc Il s'agit d'un verrou bien plus précis car il ne concerne qu'un bloc. Pour verrouiller un bloc on utilise un objet (ie une référence, un identiant d'objet), en général il est conseillé de faire usage d'un champ privé de l'objet sur lequel la méthode (le thread) s'exécute car si le verrou est public toute tache pourra verrouiller notre bloc en utilisant le même verrou ailleurs. Se verrouillage ne bloque pas les autres méthodes applicable à l'objet, il peut cependant bloquer l'accès à d'autres bloc synchronisés utilisant le même verrou. public class Compteur_Synchro { private int Monverrou; private int c = 0; .......blabla............... public void fairequelchose(int j) { blabla ... synchronized(Monverrou) { BLOC PROTËGË } } Ordonancement en Java Une instruction permet de demander à un Thread d'attendre qu'une tâche ai terminé son calcul. Cette simple instruction permet d'imposer des règles de précédence telles que A) . Si le thread TA (resp. début de l'exécution de B T B) exécute la tâche A (resp. B) A B (i.e B dépend de A < B en écrivant au doit se terminer avant on spécie en Java que : TA.join() Cette instruction bloque le l calcul du thread qui l'eectue jusqu'à ce que le thread entendu il faut le thread invoquant cette méthode puisse acceder à T A. TA soit terminé, bien Question 4 Reprendre encore une fois le code de l'exemple Petit_job.java, et demander au thread principal d'attendre que les threads t1,t2 aient terminé avant d'acher la valeur du compteur. Le résultat est il enn able ? Question 5 Écrire un programme qui crée trois threads A, B, C opérant sur un objet exécutable Dormeur La méthode run() de Dormeur se contente d'attendre quelques secondes puis avant de se terminer elle écrit un message (du type je suis XX j ai ni). le thread principal crée On veut maintenant que Syst. Distribués. 3 threads à partir de Dormeur et les démarre dans l'ordre C<B<A (donc A doit terminer en dernier, et Page 5/6 C en premier). October 14, 2016 A, B, C . T.D. 2 MIAGE Sophia Modier le code du constructeur de la classe Dormeur pour qu'il prenne en paramètre une liste de 0 ou 1 threads à attendre, alternativement dénir une méthode set_pred(Thread t) qui permet de dénir le prédecesseur d'un Dormeur. Construire C pour qu'il n'attende personne (ie Construire B pour qu'il attende C. Construire A pour qu'il attende B. Lancer les 3 ∅). threads et observer. Que ce passerait-il si on ajoutait un thread TS avec D<C et A<D ? Ce qui suit est totalement optionnel, si le coeur vous en dit donc . . . Question 6 Généraliser le programme précédent au cas de n threads T 1 < T 2 < T 3 < . . . T n ou argument passé au programme, i.e T n ne dépend de personne et T 1 est le dernier à s'exécuter. n est un Question 7 Généraliser le programme précédent au cas où les dépendances forment un arbre binaire de profondeur n, ie la tache R ne dépend de personne, R0, R1 dépendent de R, R01, R00 dépendent de R0 tandis que R10, R11 dépendent de R1 et ainsi de suite, RX1, RX0 dépendent de RX Question 8 Trouver un moyen de faire dépendre une tâche d'une liste de tâches qui ne soit pas limitée à un élément. Question 9 Supposons que nous ayons en entrée un graphe de dependance, c'est à dire un ensemble de taches T = {0, 1, 2, . . . T − 1} qui nous procure pour chaque tache t ∈ T la liste des tâches dont elle dépend. Écrire un programme qui génére les Threads avec les conditions de synchronisation assurant que l'exécution se deroulera dans le bon ordre et que les tâches réalisable en même temps pourront l'être. Le corp des Thread sera remplacer par une fonction factice (une attente , une boucle). Syst. Distribués. Page 6/6 October 14, 2016