ynchronizedaitotifyotifyAllfsystemutrintrintln
Plan
.7@percent
-
Création de processus
- Terminaison
- Variables partagées
- Atomicité et sections critiques
- Interblocage
- Famine
- Synchronisation par variables de condition
Concurrence (1/2)
-
Les programmes séquentiels exécutent
séquentiellement leurs instructions.
- Þ ils ne font qu'une seule chose à la fois.
Mais il faut :
-
programmer des machines multi-processeurs ;
(Cray; Thinking Machines; Deep Blue; Blue Gene; gros serveurs)
- gérer des événements asynchrones lents ;
(entrée au terminal, signaux d'un capteur, etc)
- s'adapter au comportement concurrent des humains ;
- gérer les systèmes multi-utilisateurs ;
- réduire les délais d'attente ;
- et aller plus vite.
Concurrence (2/2)
-
x = 3; y = 25;
peut s'exécuter en parallèle
x =3;
|| y = 25;
sur deux processeurs
x = 3; y = x;
ne s'exécute pas en parallèle
dépendances entre les deux instructions
- sur une machine vectorisée, on exécute en une seule instruction
a[i] = b[i] + c[i];
- parallélisation des programmes
automatique ou manuelle
- concurrence ¹ parallélisme
- concurrence = programmation concurrente + synchronisation
- parallélisme = optimisation des programmes pour aller plus vite
- parallélisme Þ
supercalculateurs.
Processus (1/6)
Un processus (Thread) est un programme qui s'exécute.
class Code implements Runnable {
public void run () {
while (true) {
System.out.println ("Bonjour de " + Thread.currentThread());
} }
public static void main (String[ ] args) {
Code p = new Code();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.start(); t2.start();
} } Exécution
Processus (2/6)
Un processus (Thread) est un programme qui s'exécute.
class Code implements Runnable {
public void run () {
while (true) {
System.out.println ("Bonjour de " + Thread.currentThread());
Thread.yield();
} }
public static void main (String[ ] args) {
Code p = new Code();
Thread t1 = new Thread(p);
Thread t2 = new Thread(p);
t1.start(); t2.start();
} } Exécution
Processus (3/6)
En Java
-
l'interface
Runnable
contient la
méthode run() qui sera le point d'entrée appelé
par le lancement d'un processus.
(» main mais moins
standardisé pour lui passer des arguments)
- le constructeur
Thread(p)
crée un processus à partir
d'un objet Runnable
- le lancement d'un processus se fait en appelant sa méthode start().
- ·dans l'exemple précédent, il y a 3 processus:
-
celui qui exécute le programme principal lancé par la JVM;
- t1 et t2 qui exécutent le même code p.
- ·
currentThread
est une variable statique de la classe
Thread
donnant le processus courant.
Thread.yield
est facultatif (mais aide l'ordonnanceur).
Processus (4/6)
Le même exemple en programmation par objets.
class Code1 extends Thread {
public void run () {
while (true) {
System.out.println ("Bonjour de " + this);
Thread.yield();
}
}
public static void main (String[ ] args) {
Thread t1 = new Code1();
Thread t2 = new Code1();
t1.start(); t2.start();
}
} Exécution
Plus simple,
mais moins de latitude dans la hiérarchie des classes.
Processus (5/6)
-
Les trois processus fonctionnent en parallèle;
- Si moins de 3 processeurs, il faut partager l'utilisation des
processeurs (temps partagé):
Processus (6/6)
-
les threads sont des processus légers (light-weight processes).
- les processus (lourds) sont les processus gérés par le
système d'exploitation (Unix, Linux, Windows). En Unix,
on obtient leur liste par la commande
ps
,
psaux
, top
, ...
- les processus légers sont gérés par un sous-système: ici
l'interpréteur de programmes Java (la JVM).
- ils sont légers car basculer d'un processus à un autre est
une opération peu couteuse (changement de
contexte rapide)
- ici, comme nous ne nous soucions pas du système, nous les
appelons simplement processus.
- l'exécution des processus est gérée par un ordonnanceur (scheduler), qui donne des tranches de temps à
chaque processus, aussi équitablement que possible.
- Exemples de progs/circles/Examples.html (Boussinot, ENSMP Sophia)
Terminaison (1/3)
class Exemple1 implements Runnable {
public void run () {
try {
while (true) {
System.out.println ("Bonjour de " + Thread.currentThread());
Thread.sleep(1000);
}
} catch (InterruptedException e) { }
}
public static void main (String[ ] args) {
Thread t = new Thread(new Exemple1());
t.start();
t.interrupt();
}
}
Un processus t est souvent un programme qui ne s'arrête jamais.
On peut l'interrompre: t doit alors gérer sa propre
terminaison. Ainsi les invariants sont préservés.
Terminaison (2/3)
class Exemple2 extends Thread {
public void run () {
while (!isInterrupted()) {
System.out.println ("Bonjour de " + this);
}
}
public static void main (String[ ] args) {
Thread t = new Exemple2();
t.start();
t.interrupt();
}
} Exécution
Un processus teste périodiquement s'il n'est pas interrompu.
Terminaison (3/3)
class Exemple3 extends Thread {
public void run () {
System.out.println ("Bonjour de " + this);
}
public static void main (String[ ] args)
throws InterruptedException {
Thread t = new Exemple3();
t.start();
t.join(0);
}
} Exécution
On peut attendre la fin d'un processus en précisant un délai maximum
d'attente en argument de join en millisecondes (0 = délai
infini).
La méthode join lance une exception si un autre
processus a interrompu le processus courant.
Variables partagées (1/2)
Une même variable x peut être modifiée par deux processus.
|
class P1 extends Thread {
public void run () {
while (true) {
P.x = 23;
Thread.yield();
} } } |
|
class P2 extends Thread {
public void run () {
while (true) {
P.x = 7;
Thread.yield();
} } } |
class P {
static int x = 0;
public static void main (String[ ] args)
throws InterruptedException {
Thread t1 = new P1(), t2 = new P2();
t1.start(); t2.start();
while (true) {
System.out.println (x); // ----------------- Valeur de x?
Thread.sleep (1000);
} } } Exécution
Variables partagées (2/2)
- on ne peut prédire l'ordre dans lequel P1 ou P2 écrira
la variable partagée x.
- cet ordre peut changer entre deux exécutions du même programme.
- les programmes concurrents sont non déterministes.
Atomicité et sections critiques (1/5)
Atomicité et sections critiques (2/5)
Atomicité et sections critiques (3/5)
Atomicité et sections critiques (4/5)
-
le processus qui appelle une méthode
synchronized
doit obtenir un verrou sur l'objet auquel elle
appartient.
- si le verrou est déjà pris par un autre processus,
l'appel est suspendu jusqu'à ce que le verrou soit
disponible.
- en Java, les verrous sont associés aux objets. Une seule
méthode
synchronized
peut être appelée simultanément sur
un même objet. Mais une même méthode peut être appelée simultanément
sur deux objets différents.
- une méthode
staticsynchronized
fonctionne avec un
verrou associé à toute la classe.
synchronized
n'existe que pour les méthodes et non pour l'accès à un champ de données.
- remarque: un processus, possédant déjà un verrou, peut le
reprendre sans se bloquer (¹ Threads Posix)
Þ une méthode synchronized
récursive ne se
bloque pas.
Atomicité et sections critiques (5/5)
Interblocage
-
Créer des sections critiques demande un peu d'attention, puisque des
interblocages (deadlocks) sont possibles.
|
class P1 extends Thread {
public void run () {
synchronized (a) {
synchronized (b) {
...
} } } } |
|
class P2 extends Thread {
public void run () {
synchronized (b) {
synchronized (a) {
...
} } } } |
- P1 prend le verrou sur a,
P2 prend le verrou sur b,
Þ interblocage
- Détecter les interblocages peut être très complexe.
(absence d'interblocages » terminaison des programmes)
Exercice 1 Faire un exemple d'interblocage avec des appels sur des
méthodes synchronisées.
Famine
- Rien ne garantit qu'un processus bloqué devant une section
critique passera un jour dans la section critique
Þ
Famine (starvation)
- On suppose souvent que la politique d'ordonnancement est juste et garantit de donner la main un jour à tout processus qui
l'a demandée
(fair scheduling) Þ Problème de l'équité
-
·Equité faible: si un processus est prêt à s'exécuter
continument, il finira par s'exécuter.
- ·Equité forte: si un processus est prêt infiniment à s'exécuter, il finira par s'exécuter.
- Parfois on veut le garantir logiquement en forçant un ordre de passage
entre processus. (cf. cours suivant)
Conditions (1/7)
Conditions (2/7)
-
ajouter doit attendre que la file f se vide, si f est
pleine.
- supprimer doit attendre que la file f se remplisse, si
f est vide
- il faut relacher le verrou pour
que l'autre puisse s'exécuter.
- 2 solutions:
-
·ressortir de chaque méthode sans se bloquer
et revenir tester la file Þ attente
active (busy wait) qui coûte cher en temps.
- ·atomiquement relacher le verrou + attendre sur une
condition.
- en Java, une condition est une simple référence à un objet (son
adresse). Par exemple, ici la file elle-même f.
- quand on remplit la file, on peut envoyer un signal
sur une condition et réveiller un processus en attente
sur la condition.
Conditions (3/7)
static void ajouter (int x, FIFO f) throws InterruptedException {
synchronized (f) {
while (f.pleine)
f.wait();
f.contenu = x;
f.pleine = true;
f.notify();
} }
static int supprimer (FIFO f) throws InterruptedException {
synchronized (f) {
while (!f.pleine)
f.wait();
f.pleine = false;
f.notify();
return f.contenu;
} } Exécution
-
wait et notify sont deux méthodes de la
classe Object. Tous les objets ont donc ces deux méthodes
présentes.
Conditions (4/7)
synchronized void ajouter (int x) throws InterruptedException {
while (pleine)
wait();
contenu = x;
pleine = true;
notify();
}
synchronized int supprimer () throws InterruptedException {
while (!pleine)
wait();
pleine = false;
notify();
return contenu;
} Exécution
-
Version plus en programmation par objets.
Conditions (5/7)
synchronized void ajouter (int x) throws InterruptedException {
while (pleine)
wait();
contenu = x;
pleine = true;
notifyAll();
}
synchronized int supprimer () throws InterruptedException {
while (!pleine)
wait();
pleine = false;
notifyAll();
return contenu;
} Exécution
- si plus de deux processus sont en jeu, on peut utiliser la
méthode notifyAll de la classe Object pour envoyer un
signal à tous les processus en attente sur la condition.
Conditions (6/7)
-
notify n'a pas de mémoire. On réveille
un quelconque des processus en attente sur la condition.
- l'action relachant le verrou et de mise en attente engendrée par
wait est atomique. Sinon, on pourrait perdre un
signal émis par notify avant d'attendre sur la condition.
- de même notify est fait avant de relâcher le verrou.
- il faut utiliser un
while
et non un if
dans la section critique. En effet, il se peut qu'un autre processus
soit réveillé et qu'il invalide le test.
Conditions (7/7)
class FIFO {
int debut, fin; boolean pleine, vide; int[ ] contenu;
...
synchronized void ajouter (int x) throws InterruptedException {
while (pleine)
wait();
contenu[fin] = x;
fin = (fin + 1) % contenu.length;
vide = false; pleine = fin == debut;
notifyAll();
}
synchronized int supprimer () throws InterruptedException {
while (vide)
wait();
int res = contenu[debut];
debut = (debut + 1) % contenu.length;
vide = fin == debut; pleine = false;
notifyAll();
return res;
} Exécution
Exercices
Exercice 2 Donner un exemple précis où notifyAll fait une
différence avec notify.
Exercice 3 Expliquer dans le détail pourquoi l'action notify doit
se faire en section critique. Que se passerait-il si l'instruction
était faite en dehors de la section critique?
Exercice 4 Faire l'exemple de la file d'attente avec la représentation
en liste d'éléments.
Exercice 5 Programmer des piles concurrentes.
Exercice 6 Montrer que le réveil des processus n'est pas forcément
dans l'ordre premier en attente, premier réveillé.
Exercice 7 Donner un exemple où un processus peut attendre un temps
infini avant d'entrer en section critique.
Exercice 8 Comment programmer un service d'attente où les processus
sont réveillés dans l'ordre d'arrivée.
This document was translated from LATEX by
HEVEA.