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