ynchronizedaitotifyotifyAllfsystemutrintrintln
Plan
-
Rappels
- Conditions et moniteurs
- Etats d'un processus
- Ordonnancement
- Les lecteurs et les écrivains
- Implémentation de la synchronisation
Bibliographie
Greg Nelson, Systems Programming with Modula-3, Prentice Hall, 1991.
Mordechai Ben-Ari, Principles of Concurrent Programming, Prentice Hall,
1982.
Fred B. Schneider, On Concurrent Programming, Springer, 1997.
Jean Bacon, Concurrent Systems, Addison-Wesley, 1998.
S.J. Leffler, M.K. McKusick, M.J. Karels, J.S. Quartermann,
The Design and Implementation of the 4.3 BSD Unix Operating System,
Addison Wesley 1989.
Rappels
-
un processus t (Thread) est un programme qui s'exécute;
- t.
start
lance l'exécution concurrente de la
méthode t.run
de t;
- t.
interrupt
signale qu'on demande l'interruption
de t;
isInterrupted
teste si un signal d'interruption est
arrivé;
InterruptedException
est levée quand t est en
attente (et ne peut pas tester isInterrupted
;
- les méthodes
synchronized
d'un même objet
s'exécutent en exclusion mutuelle;
wait
, notify
, notifyAll
permettent la gestion concurrente de données.
Conditions et moniteurs (1/7)
Conditions et moniteurs (2/7)
-
ajouter attend que la file f se vide, si f est
pleine.
- supprimer attend que la file f se remplisse, si
f est vide
- il faut relâcher 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 relâcher le verrou + attendre
sur une condition.
Quand la condition est notifiée, on repart en
reprenant le verrou.
- 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 envoie un signal
sur une condition et réveiller un processus en attente
sur la condition.
Conditions et moniteurs (3/7)
-
wait et notify sont deux méthodes de la
classe Object. Tous les objets ont ces deux méthodes.
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
Conditions et moniteurs (4/7)
Conditions et moniteurs (5/7)
Conditions et moniteurs (6/7)
-
notify n'a pas de mémoire. On réveille
un quelconque des processus en attente sur la condition.
- l'action relâchant le verrou et de mise en attente engendrée par
wait est atomique. Sinon, on peut 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, car un autre processus peut être réveillé et
invalider le test.
Conditions et moniteurs (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
Etats d'un processus (1/3)
-
État prêt: un processus peut s'exécuter.
- État exécution: un processus qui
s'exécute (currentThread).
- État bloqué: on attend sur une condition.
- Un signal émis sur une condition peut réveiller le processus et
le faire passer dans l'état prêt.
Etats d'un processus (2/3)
-
Il peut y avoir plus de processus prêts que de processeurs.
- Les processus prêts forment un ensemble, souvent géré
comme une FIFO ou une file de priorités (runQ)
- Le système (la JVM) en choisit un pour l'exécution. Lequel?
- C'est le problème de l'ordonnancement des processus (scheduling).
- ·Systèmes préemptifs peuvent arrêter un processus en cours
d'exécution et le mettre dans l'état prêt.
- ·Systèmes non préemptifs attendent qu'un processus relâche le processeur
(par
|Thread.yield()|). \itemBDans
les systèmes non préemptifs, les processus fonctionnent en
coroutines.
Etats d'un processus (3/3)
- création ® prêt:
|start()|
- prêt ® exécution:
ordonnanceur (scheduler)
- exécution ® prêt:
|Thread.yield()|
ou
fin d'un quantum de temps (pour systèmes préemptifs)
- exécution ® bloqué:
|wait()|,
début de synchronized
- bloqué ® prêt:
réveil sur
|notify()|
ou
|notifyAll()|,
fin de synchronized
- exécution ® mort: fin de
|run()|
Ordonnancement (1/2)
- Priorités statiques. Les méthodes getPriority. et setPriority donnent les valeurs de la
priorité d'un processus.
- Les priorités suivantes
MIN_PRIORITY
,
NORM_PRIORITY
,
MAX_PRIORITY
sont prédéfinies.
- En fait, l'ordonnanceur peut aussi affecter des priorités dynamiques pour avoir une politique d'allocation du (des)
processeur(s).
- La spécification de Java dit qu'en général les processus de forte
priorité vont s'exécuter avant les processus de basse priorité.
- L'important est d'écrire du code portable Þ
comprendre ce que la JVM garantit sur tous les systèmes.
- Ecrire du code contrôlant l'accès à des variables partagées en
faisant des hypothèses sur la priorité des processus ou sur leur
vitesse relative (race condition) est très dangereux.
Ordonnancement (2/2)
-
priorité sur l'age. Au début, p=p0 est la priorité statique donnée
au processus. Si un processus prêt attend le processeur pendant k
secondes, on incrémente p. Quand il s'exécute, on fait p = p0.
- priorité décroit si on utilise beaucoup de processeur (Unix
4.3BSD). Au début p = PUSER + 2 pnice est la priorité initiale
statique.
Tous les 40ms, la priorité est recalculée par:
p = PUSER + |
é
ê
ê
ê |
|
|
ù
ú
ú
ú |
+ 2 pnice |
Tous les 10 ms, pcpu est incrémenté quand le
processus s'exécute.
Toutes les secondes, pcpu est aussi modifié par le
filtre suivant:
qui fait baisser sa valeur en fonction de la charge globale du système
load (nombre moyen de processus prêts sur la dernière
minute).
Pour Unix, la priorité croit dans l'ordre inverse
de sa valeur (0 est fort, 127 est bas).
Synchronisation et priorités
-
Avec les verrous pour l'exclusion mutuelle, une inversion de priorités est possible.
- 3 processus Pb, Pm, Ph s'exécutent en
priorité basse, moyenne, et haute.
- Pb prend un verrou qui bloque Ph.
- Pm s'exécute et empêche Pb (de priorité plus faible)
de s'exécuter.
- le verrou n'est pas relâché et un processus de faible priorité
Pm peut empêcher le processus Ph de s'exécuter.
- on peut s'en sortir en affectant des priorités sur les verrous
en notant le processus de plus haute priorité attendant sur ce
verrou. Alors on fait monter la priorité d'un processus ayant pris
ce verrou (Win 2000).
- tout cela est dangereux.
Les lecteurs et les écrivains (1/6)
Une ressource partagée est lue ou modifiée
concurremment. En lecture, la ressource n'est pas modifiée.
- Plusieurs processus (lecteurs) peuvent lire simultanément la
donnée.
- Un seul processus (écrivain) peut modifier la donnée.
- Quand un lecteur s'exécute, aucun écrivain ne peut s'exécuter en
parallèle.
- Quand un écrivain s'exécute, aucun autre écrivain, ni aucun
lecteur, ne peut s'exécuter.
|
void lecture() {
accesPartage();
// lire la donnée partagée
retourPartage();
} |
|
void ecriture() {
accesExclusif();
// modifier la donnée partagée
retourExclusif();
} |
Les lecteurs et les écrivains (2/6)
En lecture, on a nLecteurs simultanés
(nLecteurs > 0).
En écriture, on a nLecteurs = -1.
|
synchronized void accesPartage() {
while (nLecteurs == -1)
wait();
++ nLecteurs;
}
synchronized void retourPartage() {
-- nLecteurs;
if (nLecteurs == 0)
notify();
} |
synchronized void accesExclusif() {
while (nLecteurs != 0)
wait();
nLecteurs = -1;
}
synchronized void retourExclusif() {
nLecteurs = 0;
notifyAll();
}
|
Les lecteurs et les écrivains (3/6)
notifyAll réveille trop de processus se retrouvant
immédiatement bloqués. Avec des variables de condition Posix
cLecture
et cEcriture
, on a un contrôle
plus fin:
|
synchronized void accesPartage() {
++ nLecteursEnAttente;
while (nLecteurs == -1)
waitPosix(cLecture);
-- nLecteursEnAttente;
++ nLecteurs;
}
synchronized void retourPartage() {
-- nLecteurs;
if (nLecteurs == 0)
notifyPosix(cEcriture);
} |
synchronized void accesExclusif() {
while (nLecteurs != 0)
waitPosix(cEcriture);
nLecteurs = -1;
}
synchronized void retourExclusif() {
nLecteurs = 0;
if (nLecteursEnAttente > 0)
notifyAllPosix(cLecture);
else
notifyPosix(cEcriture);
} |
Les conditions Posix n'existent pas en Java !!
Elles existent en C, C++, Ocaml, Modula-3, ADA, etc
Les lecteurs et les écrivains (4/6)
Exécuter notify à l'intérieur d'une section critique n'est pas
très efficace. Avec un seul processeur, ce n'est pas un problème car
les réveillés passent dans l'état prêt attendant la disponibilité
du processeur.
Avec plusieurs processeurs, le processus réveillé peut retomber
rapidement dans l'état bloqué, tant que le verrou n'est pas relâché.
Il vaut mieux faire notify à l'extérieur de la section
critique
(Ce qu'on ne fait jamais !),
ou au moment de la sortie du synchronized
:
void retourPartage() {
boolean faireNotify;
synchronized (this) {
-- nLecteurs;
faireNotify = nLecteurs == 0;
}
if (faireNotify)
notifyPosix(cEcriture);
}
Les lecteurs et les écrivains (5/6)
Des blocages inutiles sont possibles (avec plusieurs
processeurs) sur le notifyAll de fin d'écriture.
Comme avant, on peut le sortir de la section critique.
Si plusieurs lecteurs sont réveillés, un seul prend le
verrou. Mieux vaut faire notify en fin d'écriture,
puis refaire notify en fin d'accès partagé pour relancer les
autres lecteurs.
|
void accesPartage() {
synchronized (this) {
++ nLecteursEnAttente;
while (nLecteurs == -1)
waitPosix(cLecture);
-- nLecteursEnAttente;
++ nLecteurs;
}
notifyPosix(cLecture);
} |
|
synchronized void retourExclusif() {
nLecteurs = 0;
if (nLecteursEnAttente > 0)
notifyPosix(cLecture);
else
notifyPosix(cEcriture);
}
|
Les lecteurs et les écrivains (6/6)
Famine possible d'un écrivain en attente
de fin de lecture. La politique d'ordonnancement des processus peut
aider. On peut aussi logiquement imposer le passage d'un écrivain.
|
void accesPartage() {
synchronized (this) {
++ nLecteursEnAttente;
if (nEcrivainsEnAttente > 0)
waitPosix(cLecture);
while (nLecteurs == -1)
waitPosix(cLecture);
-- nLecteursEnAttente;
++ nLecteurs;
}
notifyPosix(cLecture);
} |
|
synchronized void accesExclusif() {
++ nEcrivainsEnAttente;
while (nLecteurs != 0)
waitPosix(cEcriture);
-- nEcrivainsEnAttente;
nLecteurs = -1;
}
|
Contrôler finement la synchronisation peut être complexe.
Exclusion mutuelle: implémentation (1/4)
Exclusion mutuelle: implémentation (2/4)
Exclusion mutuelle: implémentation (3/4)
-
Sans
TAS
, peut-on y arriver?
- Solution injuste
|
while (true) {
while (tour != 0)
;
// section critique
tour = 1;
} |
|
while (true) {
while (tour != 1)
;
// section critique
tour = 0;
} |
- Solution fausse (cf. la réservation des places d'avions)
|
while (true) {
while (actif[1])
;
actif[0] = true;
// section critique
actif[0] = false;
} |
|
while (true) {
while (actif[0])
;
actif[1] = true;
// section critique
actif[1] = false;
} |
Exclusion mutuelle: implémentation (4/4)
- Solution avec interblocage
|
while (true) {
actif[0] = true;
while (actif[1])
;
// section critique
actif[0] = false;
} |
|
while (true) {
actif[1] = true;
while (actif[0])
;
// section critique
actif[1] = false;
} |
- Solution correcte possible ?
Þ algorithmes de [Dekker, Peterson]
Algorithme de Peterson (1/5)
class Peterson extends Thread {
static int tour = 0;
static boolean[ ] actif = {false, false};
int i, j;
Peterson (int x) { i = x; j = 1 - x; }
public void run() {
while (true) {
actif[i] = true;
tour = j;
while (actif[j] && tour == j)
;
// section critique
actif[i] = false;
} }
public static void main (String[ ] args) {
Thread t0 = new Peterson(0), t1 = new Peterson(1);
t0.start(); t1.start();
} }
Algorithme de Peterson (2/5)
Preuve de sureté. (safety)
Si t0 et t1 sont tous les deux dans leur section critique. Alors
actif[0] = actif[1] = true.
Impossible car les deux tests auraient été franchis en même temps
alors que tour favorise l'un deux. Donc un seul est
entré. Disons t0.
Cela veut dire que t1 n'a pu trouver le tour à 1 et
n'est pas entré en section critique.
Preuve de vivacité. (lifeness)
Supposons t0 bloqué dans le while
.
Cas 1: t1 non intéressé à rentrer dans la section critique. Alors
actif[1] = false. Et donc t0 ne peut être bloqué par le
while
.
Cas 2: t1 est aussi bloqué dans le while
. Impossible
car selon la valeur de tour, l'un de t0 ou t1 ne peut
rester dans le while
.
Algorithme de Peterson (3/5)
Algorithme de Peterson (4/5)
Preuve de sureté:
Algorithme de Peterson (5/5)
Preuve de vivacité.
Exercices
Exercice 1 Dans le langage Ada, la communication se fait par
rendez-vous. Deux processus P et Q font un rendez-vous s'ils
s'attendent mutuellement chacun à un point de son programme.
On peut en profiter pour passer une valeur. Comment organiser cela
en Java?
Exercice 2 Une barrière de synchronisation entre plusieurs processus
est un endroit commun que tous les processus actifs doivent franchir
simultanément. C'est donc une généralisation à n processus d'un
rendez-vous. Comment la programmer en Java?
Exercice 3 Toute variable peut être partagée entre plusieurs processus,
quel est le gros danger de la communication par variables partagées?
Exercice 4 Un ordonnanceur est juste s'il garantit à tout processus
prêt de passer dans l'état exécution. Comment le construire?
Exercice 5 (difficile) Généraliser l'algorithme de Peterson au
cas de n processus (n ³ 2).
This document was translated from LATEX by
HEVEA.