0% ont trouvé ce document utile (0 vote)
113 vues188 pages

Cours Ac

Le document présente un programme de Master 2 en Informatique, avec un accent sur l'algorithmique concurrente et la synchronisation des processus. Il couvre des sujets tels que les threads Java, la synchronisation entre processus, et les systèmes distribués, incluant des algorithmes d'élection. Le contenu est structuré avec des sections détaillées et des exercices pour renforcer l'apprentissage.

Transféré par

Esther Horowitz
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
113 vues188 pages

Cours Ac

Le document présente un programme de Master 2 en Informatique, avec un accent sur l'algorithmique concurrente et la synchronisation des processus. Il couvre des sujets tels que les threads Java, la synchronisation entre processus, et les systèmes distribués, incluant des algorithmes d'élection. Le contenu est structuré avec des sections détaillées et des exercices pour renforcer l'apprentissage.

Transféré par

Esther Horowitz
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

MASTER 2 INFORMATIQUE I2A

FINANCE

HISTOIRE
MASTER MENTION INFORMATIQUE
GÉOGRAPHIE Parcours Informatique Avancé et Applications (I2A)

INFORMATIQUE

MATHÉMATIQUES

SCIENCES POUR L'INGÉNIEUR

FRANÇAIS LANGUE ÉTRANGÈRE Centre de Télé-enseignement


Universitaire
ADMINISTRATION ÉCONOMIQUE ET SOCIALE [Link]

DIPLÔME D'ACCÈS AUX ÉTUDES UNIVERSITAIRES

FILIÈRE INFORMATIQUE

VVI9EAC

Algorithmique concurrente

Mr PHILIPPE - LAURENT
[Link]@[Link]

Mme HERRMANN - BENEDICTE


[Link]@[Link]
Table des matières

Introduction 1

I Système centralisé 2

1 Thread Java (LP) 3


1.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1.1 Processus et threads . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1.2 Les threads Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Création d’un thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2.1 Création par héritage de la classe Thread . . . . . . . . . . . . . . 5
1.2.2 Création par implémentation de l’interface Runnable . . . . . . 6
1.3 Méthodes de la classe Thread . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3.1 Les constructeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3.2 Principales méthodes de la classe Thread . . . . . . . . . . . . . . 7
1.3.3 Vie des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.4 Interruption . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.5 Priorité et ordonnancement des threads . . . . . . . . . . . . . . . 10
1.4 Thread et accès à la mémoire . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5 Synthèse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.7 Solutions des exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

2 Synchronisation entre processus (BH) 24


2.1 Contexte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.1.1 L’état de concurrence . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.1.2 Exclusion mutuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.1.3 Solutions pour réaliser l’exclusion mutuelle . . . . . . . . . . . . . 32
2.2 Attente active . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

i
TABLE DES MATIÈRES

2.2.1 Attente active : les solutions logicielles . . . . . . . . . . . . . . . 33


2.2.2 Attente active : les solutions matérielles . . . . . . . . . . . . . . . 35
2.2.3 Conclusion sur l’attente active . . . . . . . . . . . . . . . . . . . . 36
2.3 Attente passive : les sémaphores . . . . . . . . . . . . . . . . . . . . . . . 37
2.3.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.3.2 Implantation des sémaphores . . . . . . . . . . . . . . . . . . . . . 38
2.3.3 Utilisation des sémaphores pour l’Exclusion Mutuelle . . . . . . . 39
2.3.4 Utilisation des sémaphores pour la Synchronisation d’Exécution
(Rendez-vous) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.3.5 Les mutex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.4 Attente passive : les moniteurs . . . . . . . . . . . . . . . . . . . . . . . . 49
2.4.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.4.2 Implantation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.4.3 Utilisation des moniteurs pour l’Exclusion Mutuelle . . . . . . . . 50
2.4.4 Utilisation des moniteurs pour la synchronisation d’exécution
ou rendez-vous . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
2.5 Les problèmes d’interblocage . . . . . . . . . . . . . . . . . . . . . . . . . 54
2.6 Les problèmes classiques de synchronisation . . . . . . . . . . . . . . . . 55
2.6.1 Producteur-Consommateur . . . . . . . . . . . . . . . . . . . . . . 55
2.6.2 Lecteurs-Rédacteurs . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.6.3 Le repas des philosophes . . . . . . . . . . . . . . . . . . . . . . . 65
2.7 Correction des Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67

3 Synchronisation en Java (LP) 91


3.1 Synchronisation des threads Java . . . . . . . . . . . . . . . . . . . . . . . 92
3.1.1 Gestion de l’exclusion mutuelle . . . . . . . . . . . . . . . . . . . . 92
3.1.2 Atomicité ou les limites de l’exclusion mutuelle . . . . . . . . . . 97
3.1.3 Synchronisation sur les objets, le modèle de moniteur Java . . . . 99
3.1.4 Risque d’interblocage . . . . . . . . . . . . . . . . . . . . . . . . . 101
3.1.5 Le package [Link] . . . . . . . . . . . . . . . 102
3.2 Problèmes classiques de synchronisation . . . . . . . . . . . . . . . . . . 102
3.2.1 Le problème des producteurs / consommateurs . . . . . . . . . . 102
3.2.2 Le problème des lecteurs/rédacteurs . . . . . . . . . . . . . . . . 103
3.3 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103

ii
TABLE DES MATIÈRES

3.4 Solutions des exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

II Système distribué 116

4 Introduction à la distribution (BH) 117


4.1 Approche distribuée . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
4.1.1 Les systèmes centralisés : rappel . . . . . . . . . . . . . . . . . . . 118
4.1.2 Les systèmes distribués . . . . . . . . . . . . . . . . . . . . . . . . 118
4.1.3 Exemples de Systèmes Distribués . . . . . . . . . . . . . . . . . . 119
4.1.4 Une couche middleware . . . . . . . . . . . . . . . . . . . . . . . . 120
4.1.5 L’algorithmique distribuée . . . . . . . . . . . . . . . . . . . . . . 121
4.2 Modélisation des systèmes distribués . . . . . . . . . . . . . . . . . . . . 121
4.2.1 Les processus logiques . . . . . . . . . . . . . . . . . . . . . . . . . 121
4.2.2 La communication . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
4.2.3 Le modèle d’exécution distribuée . . . . . . . . . . . . . . . . . . . 123
4.2.4 Le modèle algorithmique . . . . . . . . . . . . . . . . . . . . . . . 123
4.3 Evaluation des algorithmes distribués . . . . . . . . . . . . . . . . . . . . 125
4.3.1 Performance d’un algorithme distribué . . . . . . . . . . . . . . . 125
4.3.2 Propriétés d’un algorithme distribué . . . . . . . . . . . . . . . . . 125
4.4 Exemple d’application distribuée . . . . . . . . . . . . . . . . . . . . . . . 126
4.4.1 La gestion d’un garage de location de voitures . . . . . . . . . . . 126
4.4.2 Gestion de plusieurs compagnies de location de voitures . . . . . 128

5 Synchronisation entre processus dans les systèmes distribués (BH) 129


5.1 Synchronisation d’horloges . . . . . . . . . . . . . . . . . . . . . . . . . . 129
5.1.1 Exemple : absence d’une heure unique sur le programme make
de UNIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
5.1.2 Notion d’horloges logiques . . . . . . . . . . . . . . . . . . . . . . 131
5.1.3 Notion d’horloges physiques . . . . . . . . . . . . . . . . . . . . . 136
5.2 Synchronisation dans les systèmes distribués . . . . . . . . . . . . . . . . 137
5.3 Synchronisation distribuée à l’aide d’un coordinateur : mécanismes de
synchronisation centralisés . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
5.4 Synchronisation distribuée utilisant la notion d’horloges logiques . . . . 139
5.4.1 Algorithme d’exclusion mutuelle de Lamport . . . . . . . . . . . 140
5.4.2 Algorithme d’exclusion mutuelle de Ricart et Agrawala . . . . . 143

iii
TABLE DES MATIÈRES

5.4.3 Algorithme d’exclusion mutuelle de Carvalho et Roucairol . . . . 145


5.5 Synchronisation distribuée utilisant un jeton . . . . . . . . . . . . . . . . 147
5.5.1 Algorithme d’exclusion mutuelle de Le Lann . . . . . . . . . . . . 147
5.5.2 Algorithme d’exclusion mutuelle de Suzuki et Kasami . . . . . . 148
5.6 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
5.7 Correction des Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150

6 Élection dans les systèmes distribués (LP) 155


6.1 Algorithme d’élection sur un arbre . . . . . . . . . . . . . . . . . . . . . . 156
6.2 Algorithmes d’élection sur un anneau . . . . . . . . . . . . . . . . . . . . 158
6.2.1 Algorithme d’élection de Le Lann : . . . . . . . . . . . . . . . . . . 158
6.2.2 Algorithme d’élection de Chang et Roberts pour anneau . . . . . 159
6.3 Algorithme du plus fort ou Bully algorithm de Garcia-Molina . . . . . . . 159
6.4 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
6.5 Correction des Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161

7 Communication de groupe (LP) 164


7.1 Les horloges vectorielles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
7.1.1 Rappel sur l’ordre causal . . . . . . . . . . . . . . . . . . . . . . . 166
7.1.2 Rappel sur les estampilles . . . . . . . . . . . . . . . . . . . . . . . 167
7.1.3 Les historiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
7.1.4 Projection de l’historique . . . . . . . . . . . . . . . . . . . . . . . 169
7.1.5 Les horloges vectorielles . . . . . . . . . . . . . . . . . . . . . . . . 170
7.2 La diffusion fiable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
7.3 Les ordres de délivrance des messages . . . . . . . . . . . . . . . . . . . . 174
7.3.1 L’ordre FIFO (utilisé par FBCAST) . . . . . . . . . . . . . . . . . . 175
7.3.2 L’ordre causal (utilisé par CBCAST) . . . . . . . . . . . . . . . . . 175
7.3.3 L’ordre atomique ou total (utilisé par ABCAST) . . . . . . . . . . 176
7.4 Protocoles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
7.4.1 Le protocole FBCAST . . . . . . . . . . . . . . . . . . . . . . . . . 177
7.4.2 Le protocole CBCAST . . . . . . . . . . . . . . . . . . . . . . . . . 177
7.4.3 Le protocole ABCAST . . . . . . . . . . . . . . . . . . . . . . . . . 178
7.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
7.6 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

iv
Introduction

L’objectif de ce cours est d’aborder le problème de la concurrence d’exécution au


sein des systèmes informatiques. Plus particulièrement nous nous abordons la pro-
grammation concurente et les problèmes qui en découlent.
Nous parlons de concurrence d’exécution lorsque plusieurs processus 1 s’exécutent
de façon simultanée. La concurrence d’exécution engendre des problèmes, par exemple
dans l’accès à des ressources partagées. La notion de ressource critique est alors intro-
duite pour désigner une ressource devant être accédée de façon exclusive.
Ce cours aborde les problèmes de concurrence pour deux types de systèmes : les
systèmes centralisés et les systèmes distribués. Dans un système centralisé, les proces-
sus partagent des mémoires (mémoire ou fichiers), des ressources systèmes et l’hor-
loge locale de l’ordinateur, qui peuvent leur servir dans la mise en place des synchro-
nisations. Par contre dans un système distribué, les processus ne partagent pas de
mémoire et il n’existe ni horloge ni système global. Les solutions algorithmiques aux
problèmes liés à la concurrence sont donc différentes car elles ne peuvent reposer que
sur des échanges de messages.
La première partie concerne les systèmes centralisés. Après avoir introduit la pro-
grammation concurrente sur la base des threads Java au chapitre 1, nous introduisons
la notion d’exclusion mutuelle d’un point de vue plus théorique et les outils pour réa-
liser la synchronisation entre processus au chapitre 2. Nous terminons cette première
partie avec une vision pratique de la synchronisation, sur la base des threads Java au
chapitre 3.
La seconde partie du cours d’algorithmique concurrente concerne les systèmes dis-
tribués. Nous commençons cette seconde partie par une présentation d’algorithmes
gérant la synchronisation puis l’élection entre processus. Ces algorithmes reposent
principalement sur la communication entre les différents processus. La communica-
tion de groupes permet une mise en œuvre plus aisée de ces algorithmes. Elle est le
sujet du chapitre suivant. En distribué, il n’est pas toujours aisé de traiter des pro-
blèmes qui sont simples sur un seul ordinateur, ceci est illustré à travers le cas de la
détection de la terminaison d’une application. Nous donnons, dans le dernier chapitre,
une sélection d’algorithmes permettant cette détection.

1. Dans la suite, à moins que nous ne le spécifions explicitement, nous parlerons généralement de
processus pour désigner une exécution générée par un programme, une partie de processus ou un thread,
car les problématiques se posent de manière identiques dans tous ces cas.

1
Première partie

Système centralisé

2
Chapitre 1

Thread Java (LP)

Contenu
1.1 Généralités . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1.1 Processus et threads . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1.2 Les threads Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Création d’un thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2.1 Création par héritage de la classe Thread . . . . . . . . . . . . 5
1.2.2 Création par implémentation de l’interface Runnable . . . . 6
1.3 Méthodes de la classe Thread . . . . . . . . . . . . . . . . . . . . . . 7
1.3.1 Les constructeurs . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3.2 Principales méthodes de la classe Thread . . . . . . . . . . . . 7
1.3.3 Vie des threads . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.4 Interruption . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.5 Priorité et ordonnancement des threads . . . . . . . . . . . . . 10
1.4 Thread et accès à la mémoire . . . . . . . . . . . . . . . . . . . . . . . 13
1.5 Synthèse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.7 Solutions des exercices . . . . . . . . . . . . . . . . . . . . . . . . . . 18

L’objectif de ce chapitre est de présenter la programmation multi-thread en Java.


La programmation multi-thread conduit à des problèmes de concurrence comme nous
le montrons dans la partie 1.4 de ce chapitre. Elle constitue donc une base d’illusta-
tion des problèmes de synchronisation, en plus d’être une connaissance indispensable
pour la programmation des processeurs multi-cœurs.
L’utilisation de la capacité de concurrence du matériel (multiprocesseur et multi-
cœur) est rendue possible à l’intérieur des programmes grâce au support du système
d’exploitation et des langages de programmation. Les systèmes d’exploitation que
vous utilisez sur vos ordinateurs offrent tous les moyens de tirer partie de la concur-
rence matérielle à travers les notions de processus ou de Thread 1 . La plupart des lan-
gages couramment utilisés (Java, C, C++, Perl, Python, etc.) offrent également la notion
1. La traduction française du mot anglais thread est fil. Un thread représente ainsi le fil suivi par une
exécution séquentielle. Activité, processus léger, unité d’exécution ou tâche sont également utilisés pour
parler de thread.

3
Thread Java (LP)

de thread. L’exploitation entière des capacités de traitement des processeurs suppose


donc la maîtrise par le programmeur des difficultés de la programmation multi-thread
et de la concurrence qui en découle.
Dans ce chapitre nous proposons une première approche de la programmation
multi-thread, entités sur lesquelles repose généralement la concurrence au sein d’une
application. Le langage de mise en œuvre est le langage Java. Nous montrons dans
ce chapitre comment il est possible, au sein d’un programme Java, de créer et gérer
plusieurs threads pour introduire une concurrence, voire du parallélisme. Nous ne
donnons cependant pas une documentation exhaustive des threads Java et des pa-
ckages qui y sont liés. Vous pouvez vous référer à la bibliographie qui est donnée en
fin de chapitre pour plus d’information.
À l’issue de ce chapitre, vous serez en mesure de :
— utiliser les structures natives du langage Java pour créer et synchroniser des
threads ;
— discuter les limites liées à la gestion des threads Java ;

1.1 Généralités

1.1.1 Processus et threads

D’une manière générale la notion de processus, mise en place par le système, re-
couvre celle d’exécution d’une application. Dans les systèmes de type Unix/Linux un
processus est créé dans le système chaque fois qu’une nouvelle application est lan-
cée. Cette entité permet de représenter dans le système le lien qui existe entre le code
de l’application et les données qui y sont associées. Ainsi un processus classique (dit
mono-thread) représente bien la correspondance entre ce qui est mis en œuvre dans
un programme classique, séquentiel, au niveau du programmeur : l’exécution séquen-
tielle du code du programme sur ses données ; et le niveau matériel qui le prend en
charge : le processeur (mono-cœur) et la mémoire.
La notion de processus n’est cependant pas suffisante pour représenter au ni-
veau du programmeur la notion d’ordinateur multiprocesseur ou de processeur multi-
cœurs 2 . En effet dans ces architectures plusieurs supports d’exécution matériels (pro-
cesseur ou cœur) partagent la mémoire. C’est la notion de thread, portée par un pro-
cessus, qui remplit ce rôle. Ainsi un processus comportant plusieurs threads qui par-
tagent leurs données est la représentation donnée au programmeur pour correspondre
avec le niveau matériel : plusieurs cœurs qui accèdent à une mémoire commune. Au
2. A noter : il est possible d’écrire des programmes incluant plusieurs processus (par exemple avec
la fonction fork()). La différence d’une exécution multi-processus et multi-thread est que les threads
partagent l’espace mémoire alors que les processus ne le partagent pas. Attention des processus qui s’exé-
cutent sur un même ordinateur partagent les ressources gérées par le système et doivent donc prendre
en compte les problèmes de concurrence d’accès sur ces ressources.

4
Thread Java (LP)

niveau du code écrit par le programmeur le code exécuté par un thread est générale-
ment une fonction, par exemple la fonction run en Java.

1.1.2 Les threads Java

Lorsqu’un programme Java est lancé, une machine virtuelle Java (JVM : Java Vir-
tual Machine) est démarrée dans un processus et la méthode main est exécutée par
ce processus. En fait c’est un premier thread créé par défaut par la JVM qui exécute
cette fonction. Ensuite, de nouveaux threads peuvent être créés dynamiquement. Il
existe de nombreuses méthodes associées aux threads mais seules les méthodes prin-
cipales sont expliquées dans ce cours. Vous pouvez vous reporter à la documentation
disponible sur le site web de Java 3 pour compléter ce cours.

1.2 Création d’un thread

Les threads et leur gestion sont accessibles aux programmeurs Java à partir de la
classe Thread (package [Link]). En Java, un thread est toujours associé à un
objet et il n’est pas possible de lancer un thread sans un objet associé.

1.2.1 Création par héritage de la classe Thread

Une manière simple de créer un thread est de faire hériter l’objet associé de la
classe Thread, comme dans l’extrait de code donné dans le listing 1.1.

Listing 1.1 – Création de thread par héritage de classe


1 class MonThread extends Thread {
2 ...
3 public void run() {
4 ... code du thread ...
5 }
6 }

A la création d’un objet héritant de la classe Thread, un nouveau thread est au-
tomatiquement créé. Le code qui sera exécuté par le thread est alors donné dans la
méthode run(). Si un thread est associé à un objet, alors l’objet doit obligatoirement
implanter cette méthode. A son démarrage le thread exécutera cette méthode mais un
thread n’est pas démarré automatiquement à la création de l’objet ; il est à l’état passif.
Le lancement de l’exécution du thread est réalisé à l’aide de la méthode start()
qui est explicitement appelée dans le code du programme. Ce lancement fait alors
appel à la méthode run(). Il est ainsi possible de démarrer un thread à n’importe quel
3. [Link] et [Link]
[Link]

5
Thread Java (LP)

moment du déroulement du programme. L’exemple donné par le listing 1.2 montre le


code de démarrage d’un thread par le programme Principale.

Listing 1.2 – Lancement de l’exécution du thread


1 class Principale {
2 public static void main(String[] argv) {
3 MonThread p = new MonThread(...);
4 [Link]();
5 ...
6 }

Il est important de noter que lorsque la méthode start() est exécutée, le thread
est alors prêt à être exécuté. Mais il ne l’est pas forcément puisque les cœurs peuvent
être occupés par d’autres threads. Lorsque la machine virtuelle Java décide de lancer
effectivement le thread, la méthode run() de la classe à laquelle il appartient est exé-
cutée. La fin de cette méthode termine l’activité du thread. Attention, la spécification
Java ne permet pas de relancer un thread qui a fini sa méthude run(). Il faut en créer
un nouveau si nous souhaitons exécuter le même code.

1.2.2 Création par implémentation de l’interface Runnable

Comme l’héritage multiple n’existe pas en Java, il est aussi possible, pour associer
un thread à un objet, d’implémenter l’interface Runnable afin d’hériter d’une autre
classe si nécessaire.
La notion d’interface en Java est supposée connue 4 . Comme il a été dit plus haut, le
recours à l’utilisation d’une interface permet de contourner l’absence d’héritage mul-
tiple. Ainsi, si une classe supportant l’exécution de threads doit hériter d’une classe
mère, ce qui rend alors impossible de recourir directement à la classe Thread, il
est possible d’implémenter l’interface Runnable. Les méthodes d’une interface sont
toutes abstraites et doivent être écrites dans la classe qui l’implémente pour que des
objets de cette classe soient instanciables. L’interface Runnable ne contient que la mé-
thode run(). La création du thread est donc proche de ce qui a été vu précédemment.
La différence avec le recours à l’héritage de la classe Thread est que la création
d’un objet implémentant l’interface Runnable ne créé pas de thread. La création
doit être explicitement réalisée par le programmeur, directement à partir de l’objet
Thread. L’objet support, celui qui implémente l’interface Runnable, est passé dans
le constructeur de la classe Thread. Le lancement du thread est ensuite réalisé à partir
de la méthode start() du thread créé. La déclaration et le lancement d’un thread en
utilisant l’interface Runnable se fait donc comme montré dans le listing 1.3.

Listing 1.3 – Création de thread par implémentation d’interface


1 class MonThread implements Runnable {

4. Vous trouverez une explication sur : http ://[Link]/javase/tutorial/java/IandI/[Link]

6
Thread Java (LP)

2 ...
3 public void run() {
4 ... code du thread ...
5 }
6 }
7 class Principale {
8 public static void main(String[] argv) {
9 MonThread p = new MonThread(...);
10 Thread t = new Thread(p);
11 [Link]();
12 ...
13 }

1.3 Méthodes de la classe Thread

L’ensemble des méthodes de la classe Thread est donné dans la documentation


Java. Ce paragraphe reprend certaines d’entre elles et les détaille même si l’exemple
d’utilisation de l’une ou l’autre de ces méthodes est vu plus tard dans le chapitre.

1.3.1 Les constructeurs

Comme il a été vu précédemment, un thread est créé après utilisation d’un


constructeur de la classe Thread avec ou sans paramètre. Les constructeurs utilisables
sont les suivants :
— Thread(ThreadGroup, Runnable, String);
— Thread(ThreadGroup, Runnable);
— Thread(ThreadGroup, String);
— Thread(Runnable, String);
— Thread(Runnable);
— Thread(String);
— Thread();

Les paramètres sont le groupe du thread (classe ThreadGroup, il est en effet pos-
sible, suivant une hiérarchie, de créer des groupes de threads, des groupes de groupes,
etc), une classe Runnable et un nom. Par défaut le nom externe est formé de la ma-
nière suivante : "Thread"-+<entier>. Dans ce cours, nous nous limitons à l’utili-
sation des constructeurs écrits en caractères gras.

1.3.2 Principales méthodes de la classe Thread

Autres méthodes utiles à la manipulation des threads :


— static Thread currentThread() donne une référence du thread en
cours d’exécution. Cette méthode permet d’utiliser des méthodes de la classe
Thread en l’absence de référence directe sur le thread courant (par exemple

7
Thread Java (LP)

avec les threads créés en utilisant l’interface Runnable). Cette méthode est
utile par exemple pour modifier la priorité du thread courant.
— static void yield() interrompt le thread courant, ce qui oblige l’ordon-
nanceur de la JVM à reconsidérer l’attribution des cœurs aux threads, donc
éventuellement cela permet à d’autres threads de s’exécuter. Attention il n’y a
aucune garantie sur le résultat produit pas l’ordonnanceur, le comportement
de ce dernier n’étant pas fixé dans la spécification Java. 5 .
— static void sleep(long ms) throws InterruptedException
suspend l’activité du thread pendant la durée (en milisecondes) passée en
paramètre ou jusqu’à ce que son activité soit interrompue.

1.3.3 Vie des threads

La figure 1.1 illustre les différents états des threads Java. Les noms des méthodes
permettant de passer d’un état à un autre sont placés sur cette figure à titre indicatif,
leur description est donnée dans la suite de ce chapitre.

read
fin du
exécutable accept bloqué
terminé code actif (sur le CPU) (E/S)

yield
wait, join, sleep
start exécutable bloqué
créé pret
notify (synchro ou sleep)
interrupt

F IGURE 1.1 – États possibles d’un threads Java et transitions d’un état à l’autre

Méthodes ayant plus particulièrement une action sur la vie des threads :
— void start() positionne le thread dans la configuration prêt à être lancé.
Rien n’assure que l’exécution ne démarre au niveau de cette instruction.
— void join() throws InterruptedException bloque l’activité appe-
lante jusqu’à ce que le thread se termine.
— void join(long ms) throws InterruptedException attend que le
thread soit terminé ou que la durée du time out ms se soit écoulée.
— boolean isAlive() donne true si le thread a déjà été démarré au moyen
de la méthode start() et que sa méthode run() n’est pas encore terminée.
Dans le cas true, l’état du thread est soit prêt, bloqué ou en exécution.
Le listing 1.4 donne un exemple d’utilisation de la méthode join.
5. Voir [Link] et conclusion de la section 1.3.5.

8
Thread Java (LP)

Listing 1.4 – Utilisation de la fonction join


1 class essaiThread extends Thread { Donne la trace suivante :
2 private long vart;
3 public essaiThread(long t) { vart = t; } $ java essai
4 public void run(){ Thread-0 1 i = 0
5 for (int i = 0 ; i<5; i++){ Thread principal
6 [Link]( getName() +
Thread-1 2 i = 0
7 " "+ vart+" i = "+i);
8 [Link](); // passe la main Thread principal
9 } Thread-0 1 i = 1
10 } Thread-1 2 i = 1
11 }
Thread principal
12
13 class essai { Thread-0 1 i = 2
14 public static void main(String args[]) { Thread-1 2 i = 2
15 essaiThread thread1 = new Thread principal
essaiThread(1);
Thread-0 1 i = 3
16 essaiThread thread2 = new
essaiThread(2); Thread-1 2 i = 3
17 [Link](); Thread principal
18 [Link](); Thread-0 1 i = 4
19 for (int i = 0 ; i<6; i++){
Thread-1 2 i = 4
20 [Link]("Thread
principal");
Thread principal
21 [Link](); Fin thread 0!
22 } Fin thread 1!
23 try {
24 [Link]();
25 [Link]("Fin thread 0!");
26 [Link]();
27 [Link]("Fin thread 1!");
28 } catch(Exception e) {
29 [Link](e);
30 }
31 }
32 }

1.3.4 Interruption

Il est possible d’intervenir sur l’exécution des threads grâce au mécanisme d’inter-
ruption. L’appel de la méthode interrupt() sur un thread a une implication diffé-
rente en fonction de l’état du thread.
1. Si le thread sur lequel est appelée la méthode est bloqué sur une opération
d’attente suite à l’exécution de l’une ou l’autre des méthodes [Link]()
(voir la synchronisation par l’intermédiaire du mot clé synchronized dans la
section 3.1) , [Link]() ou [Link](). Dans ce cas, une excep-
tion InterruptedException est levée.

9
Thread Java (LP)

2. Si le thread sur lequel est appelée la méthode est en cours d’exécution, un in-
dicateur interrupted est positionné dans le contexte du thread. Il est alors
possible de connaître l’état cet indicateur grâce aux méthodes suivantes :
— boolean isInterrupted() renvoie la valeur de l’indicateur du thread ;
— static boolean interrupted() renvoie et efface la valeur de l’indi-
cateur du thread.
Dans le code suivant nous montrons comment un thread principal peut inter-
rompre un thread qu’il a lancé :
Thread principal Dans la classe EssaiThread
1 EssaiThread et 1 for (int i = 0; i < max; i++) {
2 = new EssaiThread(); 2 try {
3 [Link](); 3 [Link](20000);
4 [Link](10000); 4 } catch (InterruptedException e) {
5 [Link](); 5 [Link]("_Interrupted_");
6 ... 6 return;
7 }
8 }

ou bien :
1 for (int i = 0; i < max; i++) {
2 travaille(truc); // > 10000 ms
3 if ( [Link]() ) {
4 [Link]("Interrupted thread");
5 }
6 }

Ici le thread principal crée un nouveau thread (et), se met en attente puis appelle
la méthode interrupt() sur le thread qu’il a créé. Dans la classe EssaiThread,
suivant le code exécuté par le thread associé, le résultat diffère. Dans le premier cas,
le thread est en attente dans la fonction sleep. Il sortira donc de la fonction en trai-
tant l’exception InterruptedException. Dans le second cas, le thread est en train
de s’exécuter lorsque l’appel à interrupt() est réalisé. L’information de l’appel est
donc uniquement enregistrée et le thread peut y accéder plus tard à l’aide de la fonc-
tion interrupted.
A travers les interactions entre thread qu’il permet, le mécanisme d’interruption
peut être utilisé pour mettre en œuvre des synchronisations.
Attention, ce mécanisme ne concerne pas les entrées/sorties qu’il est impossible
d’interrompre et pour lesquelles aucune exception n’est levée.

1.3.5 Priorité et ordonnancement des threads

A chaque thread est attaché une priorité qui peut agir sur son ordre d’exécution. La
priorité d’un thread va de Thread.MIN_PRIORITY (1) à Thread.MAX_PRIORITY

10
Thread Java (LP)

(10). Par défaut, la priorité du thread principal main est de Thread.NORM_PRIORITY


(5). Il est possible de connaître et de modifier le niveau de priorité des threads à travers
les méthodes suivantes :
— int getPriority() donne la priorité du thread ;
— void setPrority(int p) fixe la priorité du thread à la valeur minimale
entre p et la valeur maximale permise par le groupe auquel appartient le thread.
Par défaut, un thread a la même priorité que son thread père, celui qui le crée. At-
tention, la spécification Java ne permet pas de s’appuyer sur la gestion des priorités
pour assurer le bon fonctionnement d’un programme. En effet, la seule garantie ap-
portée par la spécification est qu’un thread de plus forte priorité s’exécutera au moins
autant qu’un thread de priorité plus basse 6 . Au mieux l’ordonnanceur attribuera plus
de temps processeur aux threads les plus prioritaires au pire la priorité ne sera pas
prise en compte et l’ordre d’exécution sera indéterminé comme dans l’exemple donné
par le listing 1.5.
6. Dans la javadoc d’Oracle, il est écrit : “Threads with higher priority are executed in preference to
threads with lower priority”

11
Thread Java (LP)

Listing 1.5 – Priorité des threads


1 class prioThread extends Thread { Donne la trace suivante :
2 public prioThread(String name) {
3 setName(name); $ java Priorite
4 } Thread-0
5 public void run(){ Thread-0
6 for (int i = 0 ; i<5; i++){
Thread-0
7 [Link](getName());
8 } Thread-0
9 } Thread-0
10 } Debut Thread-0
11
Debut Thread-1
12 public class Priorite {
13 Thread principal
14 public static void main(String args[]) { Thread principal
15 Thread principal
16 prioThread t0 = new prioThread("Thread-0");
Thread principal
17 prioThread t1 = new prioThread("Thread-1");
18 [Link]( Thread.MIN_PRIORITY ); Thread principal
19 [Link]( Thread.MAX_PRIORITY ); Thread principal
20 Thread-1
21 [Link]();
Thread-1
22 [Link]("Debut " + [Link]());
23 [Link]();
Fin Thread-0
24 [Link]("Debut " + [Link]()); Thread-1
25 Thread-1
26 for (int i = 0 ; i<6; i++){ Thread-1
27 [Link]("Thread principal");
28 }
Fin Thread-1
29
Sur un processeur bi-cœurs
30 try {
31 [Link](); sous le système Ubuntu 20.04
32 [Link]("Fin " + [Link]()); et avec la JVM 16 de Oracle.
33 [Link]();
34 [Link]("Fin " + [Link]());
35 } catch(Exception e) {
36 [Link](e);
37 }
38 }
39 }

Nous voyons bien ici que les priorités ne sont pas prises en compte puisque le
thread qui s’exécute ne premier est le thread 0, alors qu’il a la priorité MIN_PRIORITY,
que le thread main a la priorité NORM_PRIORITY et que le thread 1 a la priorité
MAX_PRIORITY. Cette fonctionnalité de peut donc pas être utilisée de manière por-
table pour mettre en œuvre une synchronisation.

12
Thread Java (LP)

1.4 Thread et accès à la mémoire

Les threads issus d’un même programme principal partagent le même espace
d’adressage (la même mémoire). Les variables partagées sont les variables de classe
accessibles par toutes les instances de cette classe. C’est à dire les variables static
ou les variables accédées à partir d’une référence d’objet. Par contre les variables lo-
cales à une classe héritant de Thread reste privées au thread. L’exemple donné par le
listing 1.6 permet d’illustrer ce partage de la mémoire :

Listing 1.6 – Accès à la mémoire


1 class Partage { Donne la trace suivante :
2 public int value;
3 Partage(int val) { value = val; } statVal: 15 [Link]: 25
4 } statVal: 25 [Link]: 15
5 Final: statVal: 25 [Link]: 15
6 class EssaiMemoire extends Thread {
7 private static int statVal = 0;
8 private int localVal;
9 private Partage part;
10
11 EssaiMemoire ( int val, Partage p ) {
12 localVal = val;
13 part = p;
14 }
15
16 public void run() {
17 statVal = statVal + localVal;
18 [Link] = [Link] - localVal;
19 [Link]("statVal: " + statVal +
" [Link]: " + [Link] );
20 }
21
22 public static void main(String[] args) {
23 Partage p = new Partage( 40 );
24 Thread t1 = new EssaiMemoire ( 15, p );
25 Thread t2 = new EssaiMemoire ( 10, p );
26 try {
27 [Link]();
28 [Link]();
29 [Link]();
30 [Link]();
31 } catch (InterruptedException e){ }
32 [Link]( "Final: statVal: " +
statVal + " [Link]: " + [Link] );
33 }
34 }

Les instructions join() ont été utilisées pour que le thread principal puisse
contrôler l’exécution des threads EssaiM emoire et donner la valeur finale après qu’ils

13
Thread Java (LP)

aient tous été exécutés. En l’absence de ces instructions, la trace observée pourrait être
tout autre :
— si le thread principal termine avant les threads t1 et t2, la valeur affichée de
startVal est 0 et la valeur de [Link] est 40 ;
— si le thread principal termine après le thread le t1, mais avant le thread t2, les
valeurs affichées sont respectivement 15 et 25 et 10 si c’est l’inverse ;
— si les deux threads t1 et t2 terminent avant le thread principal, la valeur affi-
chée à la fin est correcte, quelque que soit l’ordre d’exécution de t1 et t2.
Nous voyons ici que l’ordre d’exécution de deux threads, s’il n’est pas contrôllé,
peut conduire à un résultat de programme indéterministe ce qui n’est évidemment
pas souhaitable pour un programme. De plus, la valeur affichée par les threads peut,
elle, varier car le bloc de code de l’affectation de la variable et de l’affichage ne sont
pas liés. Ainsi, si les deux threads s’exécutent exactement de manière simultanée, sur
deux cœurs distincts, ils vont réaliser les affectations puis l’affichage en parallèle. Dans
ce cas, l’affichage donnera directement la valeur finale pour les deux threads. Vous
pouvez modifier la place des instructions start et join dans le code pour vous en
convaincre.
Ainsi, en l’absence de contrôle au niveau de l’exécution des threads, exécuter cor-
rectement l’application pose problème. En effet, sans y prendre garde, l’asynchro-
nisme de l’exécution et le partage de l’espace mémoire peut conduire à des exécutions
dont le résultat n’est pas déterministe 7 . On prend ainsi conscience de la difficulté de
manipuler ce type de contexte d’exécution. Nous sommes typiquement dans la pro-
blématique posée par la synchronisation.
A titre d’illustration, lisez le code du listing 1.7 et essayez de prédire la valeur
finale de val. Exécutez ensuite le code pour vérifier que le résultat est juste :

Listing 1.7 – Compteur multi-threadé


1 class MonThread extends Thread {
2 static int val;
3
4 public void run() {
5 for ( int i = 0 ; i < 10000 ; i++ ) val++;
6 [Link]( " val= " + val);
7 }
8 }
9
10 public class counterMultiCore {
11 public static void main(String args[]) {
12 try {
13 MonThread mt1 = new MonThread();
14 MonThread mt2 = new MonThread();
15
16 [Link]();

7. L’incrémentation concurrente de la variable statVal peut d’ailleurs conduire à d’autres pro-


blèmes car cette opération n’est pas atomique.

14
Thread Java (LP)

17 [Link]();
18 [Link]();
19 [Link]();
20
21 } catch ( Exception ex ) { [Link]( ex ); }
22 }
23 }

1.5 Synthèse

Nous avons vu que la notion de thread permet de mettre en œuvre des activités
concurrentes au sein d’une application. Cette possibilité est applicable à des besoins
de programmation très divers. Parmi ceux-ci nous donner différents exemples :
— La possibilité de faire des calculs ou toute autre activité comme gérer un affi-
chage tout en mettant un thread en attente sur saisie de la part de l’utilisateur.
Imaginons que nous disposons d’une image sur laquelle nous appliquons des
filtres successifs et que nous souhaitons que l’utilisateur nous dise quand le
rendu lui convient, et donc quand arrêter l’application des filtres. En mode sé-
quentiel, nous sommes obligés de demander après chaque filtre si l’utilisateur
souhaite continuer. Avec des threads nous pouvons appliquer les filtres et affi-
cher le rendu de manière continue avec un premier thread et avoir un second
thread qui est en attente d’un signal de l’utilisateur, par exemple avec un bou-
ton ou une saisie clavier.
— Dans le cas d’un serveur (par exemple un serveur WEB), l’utilisation de threads
permet une programmation simple du traitement des requêtes. Il suffit en effet
de créer un nouveau thread à chaque nouvelle requête. Le thread exécute alors
la fonction de traitement de la requête sur les paramètres donnés reçus. De
plus, comme l’ensemble des threads partagent leurs données, il est possible de
conserver des informations sur le déroulement des requêtes ce qui n’est pas
possible en créant de nouveaux processus avec la primitive fork().
— L’utilisation des threads avec des socket est simplifie également la program-
mation en évitant le recours aux sockets non-bloquantes ou à la primitive
select().
Remarque : Dans les systèmes d’exploitation et les langages, la notion de processus, ou
de thread, est apparue indépendemment de la capacité de concurrence du matériel. Dans le cas
où un seul support d’exécution (cœur) est disponible, le système peut réaliser le partage de ce
support en arrêtant et relançant l’exécution des processus ou threads avant même la fin de leur
exécution. Le système (scheduler) attribue alors cycliquement une part du support d’exécution
pendant un intervalle de temps (quantum de temps) à chacune des entités. On parle de temps-
partagé. Ceci donne l’illusion que les exécutions sont réalisées en parallèle et les problèmes de
concurrence sont alors identiques à ceux posés si les exécution ont lieu en concurrence réelle.
A noter que le temps partagé est également utilisé pour ne pas bloquer les threads lorsque leur

15
Thread Java (LP)

nombre dépasse le nombre de support d’exécution.


Nous avons mis en évidence dans ce chapitre des difficultés liées à la program-
mation concurrente. Nous abordons dans le chapitre suivant les bases théoriques et
les solutions algorithmiques nécessaires à la résolution des problèmes de concurrence
dans un système centralisé.

1.6 Exercices

Exercice 1 : Les threads donnent l’alerte

Le but de cet exercice est l’écriture d’une classe Alerte qui gère l’affichage à ré-
pétition d’un message. La création d’une alerte par le programme principal doit en-
traîner cinq affichages successifs d’une chaîne (message d’alerte) passée en paramètre
à une seconde d’intervalle. Un niveau de priorité de l’alerte est également donné lors
de la demande de création. Comme le programme principal peut avoir à demander
plusieurs affichages, il faut qu’il puisse créer une nouvelle alerte à tout moment sans
être bloqué par les alertes en cours. L’affichage se fait donc en parallèle.
Pour ce faire, l’affichage de chaque alerte est géré par un thread dédié et le niveau
de priorité du thread est associé au niveau de priorité de l’alerte 8 .

Question 1.1 : Héritage de la classe Thread


➽ Écrire un programme Java composé de deux classes :
— la classe Alerte hérite de la classe Thread et permet de définir le traitement
de l’affichage des messages d’alerte ;
— la classe Pompier contenant le programme principal lance régulièrement des
messages d’alertes concurrents à différents niveaux de priorité en construisant
des objets de type Alerte.
Noter qu’il n’y a pas de section critique et il n’est donc pas nécessaire d’avoir re-
cours à des mécanismes d’exclusion mutuelle et de synchronisation.

Question 1.2 : Interface Runnable


➽ Écrire le même programme à la différence que la classe Alerte implémente l’in-
terface Runnable.

Exercice 2 : Parallélisme

8. Nous supposerons dans cet exercice que le niveau de priorité est correctement pris en compte par
la JVM, contrairement à ce que nous avons noté dans la description de la fonctionnalité

16
Thread Java (LP)

Le but de cet exercice est d’utiliser le parallélisme pour trouver plus rapidement le
plus grand élément d’un tableau.
➽ Écrire un programme Java qui crée un tableau d’entiers et l’initialise avec des va-
leurs aléatoires. Le programme recherche ensuite la plus grande valeur du tableau de
manière séquentielle (sans créer de thread) et avec deux threads.

17
Thread Java (LP)

1.7 Solutions des exercices

Correction Exercice 1 : Les Threads donnent l’alerte

Solution question 1.1 : Héritage de la classe Thread


➽ Écriture du programme de gestion de l’affichage des alertes grâce à des threads
obtenus par l’héritage de la classe Thread.

Listing 1.8 – Classe Alerte


1 class Alerte extends Thread {
2
3 static int nbAlerte = 0;
4 String messageAlerte;
5 int urgence; // 1..10
6
7 // contructeurs
8 Alerte (String chaine, int urgence) {
9 [Link] = chaine;
10 if (( urgence <= 10 ) && ( urgence > 0 )) {
11 [Link] = urgence;
12 }
13 else {
14 [Link] = 5;
15 }
16 }
17
18 Alerte (String chaine) {
19 [Link] = chaine;
20 [Link] = 5;
21 }
22
23 // methode lancer a la creation du thread
24 public void run() {
25 nbAlerte ++;
26 setPriority(urgence);
27 for ( int i = 0 ; i < 5 ; i ++ ){
28 [Link](nbAlerte + " alerte(s) en cours " + " "
29 + messageAlerte + " de niveau "
30 + [Link]().getPriority());
31
32 try {
33 [Link]().sleep(1);
34 }
35 catch (InterruptedException e){ ... };
36 }
37 }
38 }

18
Thread Java (LP)

Listing 1.9 – Classe Pompier


1 class Pompier {
2 public static void main(String[] argv) {
3
4 int i;
5 // trois messages d’alerte sont diffuses avec differente priorites
6 Alerte a1 = new Alerte("Attention accident RN57");
7 Alerte a2 = new Alerte("Attention risque de verglas",
8 Thread.MIN_PRIORITY);
9 Alerte a3 = new Alerte("Evacuation du batiment : vapeurs toxiques",
10 Thread.MAX_PRIORITY);
11
12 [Link]("Pompier : le 18");
13
14 [Link]();
15 [Link]();
16 [Link]();
17
18 [Link]("Pompier : le 18");
19 }
20 }

➽ La sortie écran peut être la suivante :

enice[SynchroJava]% java Pompier


Pompier : le 18
Pompier : le 18
1 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention risque de verglas de niveau 1
fenice[SynchroJava]% java Pompier

➽ Une autre sortie écran suite à l’exécution du même programme :

fenice[SynchroJava]% java Pompier


Pompier : le 18
1 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention accident RN57 de niveau 5
Pompier : le 18
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10

19
Thread Java (LP)

3 alerte(s) en cours Attention risque de verglas de niveau 1


3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention risque de verglas de niveau 1
fenice[SynchroJava]%

Solution question 1.2 : Interface Runnable


➽ Écriture du programme de gestion de l’affichage des alertes grâce à des threads
obtenus en implémentant d’interface Runnable.

Listing 1.10 – Classe Alerte


1 class Alerte implements Runnable {
2 static int nbAlerte = 0;
3 String messageAlerte;
4 int urgence; // 1..10
5
6 // contructeurs
7 Alerte (String chaine, int urgence) {
8 [Link] = chaine;
9 if (( urgence <= 10 ) && ( urgence > 0 )) {
10 [Link] = urgence;
11 } else {
12 [Link] = 5;
13 }
14 }
15
16 Alerte (String chaine) {
17 [Link] = chaine;
18 [Link] = 5;
19 }
20
21 // methode lancer a la creation du thread
22 public void run() {
23 nbAlerte ++ ;
24 [Link]().setPriority(urgence);
25 for ( int i = 0 ; i < 5 ; i ++ ){
26 [Link](nbAlerte + " alerte(s) en cours " + " "
27 + messageAlerte + " de niveau "
28 + [Link]().getPriority());
29 try {
30 [Link]().sleep(1);
31 }
32 catch (InterruptedException e){ ... };
33 }
34 }
35 }

20
Thread Java (LP)

Listing 1.11 – Classe Pompier


1 class Pompier {
2 public static void main(String[] argv) {
3
4 int i;
5 // trois messages d’alerte sont diffuses avec differente priorites
6 Alerte a1 = new Alerte("Attention accident RN57");
7 Alerte a2 = new Alerte("Attention risque de verglas",
8 Thread.MIN_PRIORITY);
9 Alerte a3 = new Alerte("Evacuation du batiment : vapeurs toxiques",
10 Thread.MAX_PRIORITY);
11
12 // creation des trois threads
13 Thread t1 = new Thread(a1);
14 Thread t2 = new Thread(a2);
15 Thread t3 = new Thread(a3);
16
17 [Link]("Pompier : le 18");
18
19 [Link]();
20 [Link]();
21 [Link]();
22
23 [Link]("Pompier : le 18");
24 }
25 }

➽ La sortie écran peut être la suivante :

fenice[SynchroJava]% java Pompier


Pompier : le 18
1 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
Pompier : le 18
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Evacuation du batiment : vapeurs toxiques de niveau 10
3 alerte(s) en cours Attention accident RN57 de niveau 5
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention risque de verglas de niveau 1
3 alerte(s) en cours Attention risque de verglas de niveau 1
fenice[SynchroJava]%

Correction Exercice 2 : Parallélisme

21
Thread Java (LP)

Le programme est décomposé en deux classes Calcul et Parallel. La première


classe hérite de thread, elle calcule le plus grand en entier d’un tableau passé en para-
mètre, à partir du point de départ donné.

Listing 1.12 – Classe Calcul


1 class Calcul extends Thread {
2
3 int tablo[];
4 int start;
5 int end;
6 int best;
7
8 // contructeurs
9 Calcul ( int[] t, int s, int e) {
10 [Link] = t;
11 [Link] = s;
12 [Link] = e;
13 }
14
15 public int getResult() {
16 return best;
17 }
18
19 // methode lancee a la creation de l’activite
20 public void run() {
21
22 best = tablo[ start ];
23 for( int i = start ; i < end ; i++ ) {
24 if ( tablo[ i ] > best ) {
25 best = tablo[ i ];
26 }
27 }
28 }
29 }

La seconde classe, la classe principale Parallel, crée un tableau d’entier qu’elle


initialise avec des valeurs aléatoires puis elle crée deux objets de type Calcul aux-
quels elle passe le tableau et leur point de départ. La création de ces deux objets en-
gendre la création thread. Le programme fait ensuite le calcul de la plus grande valeur
de manière séquentielle. Ensuite il démarre les deux threads pour le calcul parallèle.
Le thread principal attend la fin de ces deux thread avant de calculer le résultat final.

Listing 1.13 – Classe Parallel


1 public class Parallel {
2 public static void main(String[] argv) {
3
4 // Creation du tableau
5 int tabloEntier[] = new int[500000000];
6 Random rand = new Random();
7 for ( int i = 0 ; i < [Link] ; i++ ) {
8 tabloEntier[ i ] = [Link]();

22
Thread Java (LP)

9 }
10
11 // Calcul sequentiel
12 long debut = [Link]();
13 int best = tabloEntier[ 0 ];
14 for( int i = 0 ; i < [Link] ; i++ ) {
15 if ( tabloEntier[ i ] > best ) {
16 best = tabloEntier[ i ];
17 }
18 }
19 long fin = [Link]();
20
21 [Link]("Plus grand = " + best + " en " + (fin-debut));
22
23 // Creation des threads pour le calcul parallele
24 Calcul c1 = new Calcul(tabloEntier, 0, [Link]/2);
25 Calcul c2 = new Calcul(tabloEntier, [Link]/2,
[Link]);
26
27 // Calcul parallele avec deux threads
28 debut = [Link]();
29
30 [Link]();
31 [Link]();
32
33 try{
34
35 [Link]();
36 [Link]();
37
38 } catch (InterruptedException ie) {
39 [Link]("Exception error");
40 }
41
42 int res1 = [Link]();
43 int res2 = [Link]();
44 int res = (res1 < res2) ? res2 : res1;
45 fin = [Link]();
46
47 [Link]("Plus grand = " + res + " en " + (fin-debut) );
48 }
49 }

23
Chapitre 2

Synchronisation entre processus


(BH)

Contenu
2.1 Contexte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.1.1 L’état de concurrence . . . . . . . . . . . . . . . . . . . . . . . . 25
2.1.2 Exclusion mutuelle . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.1.3 Solutions pour réaliser l’exclusion mutuelle . . . . . . . . . . . 32
2.2 Attente active . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.2.1 Attente active : les solutions logicielles . . . . . . . . . . . . . 33
2.2.2 Attente active : les solutions matérielles . . . . . . . . . . . . . 35
2.2.3 Conclusion sur l’attente active . . . . . . . . . . . . . . . . . . 36
2.3 Attente passive : les sémaphores . . . . . . . . . . . . . . . . . . . . . 37
2.3.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
2.3.2 Implantation des sémaphores . . . . . . . . . . . . . . . . . . . 38
2.3.3 Utilisation des sémaphores pour l’Exclusion Mutuelle . . . . . 39
2.3.4 Utilisation des sémaphores pour la Synchronisation d’Exécu-
tion (Rendez-vous) . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.3.5 Les mutex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.4 Attente passive : les moniteurs . . . . . . . . . . . . . . . . . . . . . . 49
2.4.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.4.2 Implantation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.4.3 Utilisation des moniteurs pour l’Exclusion Mutuelle . . . . . . 50
2.4.4 Utilisation des moniteurs pour la synchronisation d’exécu-
tion ou rendez-vous . . . . . . . . . . . . . . . . . . . . . . . . 52
2.5 Les problèmes d’interblocage . . . . . . . . . . . . . . . . . . . . . . . 54
2.6 Les problèmes classiques de synchronisation . . . . . . . . . . . . . 55
2.6.1 Producteur-Consommateur . . . . . . . . . . . . . . . . . . . . 55
2.6.2 Lecteurs-Rédacteurs . . . . . . . . . . . . . . . . . . . . . . . . 61
2.6.3 Le repas des philosophes . . . . . . . . . . . . . . . . . . . . . 65
2.7 Correction des Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . 67

24
Synchronisation entre processus (BH)

Dans ce second chapitre, nous nous intéressons aux problèmes liés à la concur-
rence dans les systèmes centralisés de manière générale, algorithmique, sans prendre
en compte un langage de programmation particulier. Nous considérons que les pro-
cessus s’exécutent sur des machines mono-processeur, multi-processeurs à mémoire
partagée ou multi-cores. Ils partagent donc de la mémoire. Ces processus peuvent être
des processus légers ou threads. Les exécutions de ces processus peuvent être entrela-
cées et/ou parallèles (i.e., concurrentes).

2.1 Contexte

2.1.1 L’état de concurrence

Des processus qui travaillent ensemble sur une même machine peuvent partager
un espace de stockage. Il peut s’agir d’un fichier ou alors d’un espace dans la mémoire
principale. L’accès à cet espace de stockage partagé par les différents processus peut
poser certains problèmes liés à la concurrence d’exécution. Les processus s’exécutent
sur le (ou les) processeur(s) en temps partagé et en parallèle. De plus, si le nombre de
processeurs est inférieur au nombre de processus, les exécutions peuvent être inter-
rompues régulièrement, ses exécutions sont dites entrelacées.

Etat de concurrence ou condition de concurrence : une situation où plusieurs


processus accèdent et manipulent en concurrence les mêmes données et où le résultat
de l’exécution dépend de l’ordre particulier dans lequel on accède aux instructions.

Exemple 1 : Partage d’écran

Deux processus Proc1 et Proc2 partagent un écran pour afficher des adresses com-
posées de plusieurs champs. Chaque adresse est composée de trois champs : numéro,
rue, ville. Le tableau 2.1 donne l’exemple des adresses de deux personnes, nom-
mées respectivement ind1 et ind2 .

numéro rue ville


ind1 17 rue des chalets Besançon
ind2 5 rue des granges Besançon

TABLE 2.1 – Adresses de ind1 et ind2

25
Synchronisation entre processus (BH)

Algorithme 1 : Partage d’écran


Variables :
ind1 ,ind2 : individu
Fonction AfficherIndividu(i :individu)
begin
// affiche l’adresse de l’individu i
afficher(i..numero)
afficher([Link])
afficher([Link])
end
Algorithme de chaque Processus Pi
begin
...
AfficherIndividu(indi )
...
end

Supposons que le processus P1 désire afficher l’adresse de la personne ind1 et que


le processus P2 désire afficher l’adresse de la personne ind2 . Ainsi, le processus P1
exécute la fonction AfficherIndividu(ind1 ) le processus P2 exécute la fonction
AfficherIndividu(ind2 ) (fonction donnée dans l’algorithme 1).
L’affichage est réalisé champ par champ. Ainsi selon les cas d’exécutions entre-
lacées et/ou parallèles de ces deux processus, les affichages des adresses de ind1
et ind2 risquent d’être intercalées et donc non satisfaisantes. On peut obtenir par
exemple, l’affichage suivant sur l’écran :

adresse de ind1 : adresse de ind2: 17 5 rue des chalets Besançon


rue des granges Besançon

Exemple 2 : Accès concurrent à une adresse

Supposons dans cet exemple, que deux processus P1 et Q1 accèdent de façon


concurrente à l’adresse de la personne ind1 . P1 modifie cette adresse et Q1 l’af-
fiche. Ainsi, P1 appelle la fonction ModifierIndividu(ind1 , 4, ”rue de la
Préfecture”,Besançon”) et Q1 appelle la fonction AfficherIndividu(ind1 ).
Ces fonctions sont données dans l’algorithme 2.
Selon les cas d’exécution entrelacée de ces deux processus, l’adresse de ind1 affi-
chée par Q1 risque d’être incorrecte.

26
Synchronisation entre processus (BH)

Algorithme 2 : Accès concurrent à une adresse


Variables :
ind1 : individu
Fonction AfficherIndividu(i :individu)
begin
// affiche l’adresse de l’individu i
afficher(i..numero)
afficher([Link])
afficher([Link])
end
Fonction ModifierIndividu(i :individu, num : entier, rue : chaine, ville : chaine)
begin
// modifie l’adresse de l’individu i
[Link] ← num
[Link] ← rue
[Link] ← ville
end
Algorithme de chaque Processus Pi
begin
...
AfficherIndividu(ind1 )
...
end
Algorithme de chaque Processus Qi
begin
...
AfficherIndividu(ind1 )
...
end

27
Synchronisation entre processus (BH)

2.1.2 Exclusion mutuelle

L’exclusion mutuelle est introduite pour solutionner les problèmes liés aux états de
concurrence. Dans ce paragraphe, nous définissons les notions de : ressource critique,
exclusion mutuelle et section critique.

Une ressource critique est une entité partagée qui ne doit pas être accédée par plu-
sieurs processus à la fois.

Ainsi, si nous reprenons les exemples de la section précédente, dans l’exemple 1 la


ressource critique est l’écran et pour l’exemple 2, l’adresse de ind1 est une ressource
critique.

L’exclusion mutuelle vise à interdire le fait que des processus lisent ou écrivent
des données partagées simultanément, garantissant que si un processus utilise une
ressource critique, les autres processus ne pourront pas accéder à cette ressource. Nous
pouvons définir l’exclusion mutuelle comme suit :

L’exclusion mutuelle garantit à un processus l’accès exclusif à une ressource pen-


dant une suite d’opérations avec cette ressource.

L’exclusion mutuelle pouvant s’appliquer à plusieurs opérations sur une ressource


critique, la notion de Section Critique est introduite :

Une section critique ou région critique est une séquence d’actions d’un processus
pendant laquelle il doit être le seul à utiliser la ressource critique.

En résumé, lorsque plusieurs processus ont accès à une Ressource Critique, en uti-
lisant chacun une séquence d’instructions, appelée Section Critique, cet accès est
contrôlé de telle sorte que lorsque les demandes d’accès sont concurrentes, un pro-
cessus au plus puisse exécuter sa Section Critique. L’Exclusion Mutuelle représente le
contrôle de cet accès.

28
Synchronisation entre processus (BH)

Exemple : partage d’une voie unique de chemin de fer La figure 2.1 illustre
l’exemple classique d’exclusion mutuelle constitué par la voie unique d’un chemin
de fer :

F IGURE 2.1 – Voie unique de chemin de fer

Sur une voie unique de chemin de fer, un seul train peut passer à la fois. La voie
unique représente ici la ressource critique. L’accès à cette voie unique se fait en exclu-
sion mutuelle.

Modélisation de l’exclusion mutuelle :

L’exclusion mutuelle modélise l’accès cohérent à des ressources partagées.


Quand un processus veut accéder à la Section Critique, il appelle une fonction
d’entrée en Section Critique. Cette fonction permet à un processus d’entrer en Section
Critique, mais à un au plus, les autres processus devant attendre.
Quand il quitte la Section Critique, le processus appelle une fonction de sortie de
la Section Critique. Cette fonction signale aux processus en attente qu’il n’y a plus de
processus en Section Critique et éventuellement laisse entrer un processus et un seul
en Section Critique.

Algorithme 3 : Exclusion mutuelle


entreeSC();
Section Critique();
sortieSC();

Exemple : Partage d’écran en exclusion mutuelle Reprenons l’exemple du partage


d’écran par deux processus P1 et P2 . Pour garantir que l’affichage de l’adresse d’un in-
dividu est réalisée en exclusion mutuelle, nous introduisons une Section Critique SC
dans la fonction AfficherIndividu(i) (algorithme 4). En effet, lorsqu’un proces-
sus est en Section Critique, un autre processus ne peut pas entrer à son tour en Section
Critique : il doit attendre que le premier processus libère la Section Critique. Ainsi, les
adresses affichées par les processus P1 et P2 ne seront pas intercalées.

29
Synchronisation entre processus (BH)

Algorithme 4 : Partage d’écran en exclusion mutuelle


Variables :
ind1 , ind2 : individu
SC : SectionCritique
Fonction AfficherIndividu(i :individu)
begin
// affiche l’adresse de l’individu i
entreeSC(SC)
afficher(i..numero)
afficher([Link])
afficher([Link])
sortieSC(SC)
end
Algorithme de chaque Processus Pi
begin
...
AfficherIndividu(ind1 )
...
end

Résultat d’exécution :
Si le processus P1 exécute en premier l’appel à entreeSC(), alors il accède à la Section
Critique et cela garantit qu’il pourra exécuter la suite de son code (jusqu’à l’appel
sortieSC()) en exclusion mutuelle. Si lors de cette exécution, P2 fait appel à entreeSC(),
il sera mis en attente jusqu’à ce que P1 libère la Section Critique. Le même schéma
s’applique si c’est P2 qui exécute en premier l’appel à entreeSC(). Ainsi, les adresses
seront affichées correctement quelques soient les exécutions entrelacées.

Exemple : Accès concurrents à une adresse en exclusion mutuelle Reprenons


l’exemple de l’accès concurrent à une adresse par deux processus de la section pré-
cédente.
Pour garantir que ces accès soient réalisés en exclusion mutuelle, nous utilisons
la Section Critique SC introduite dans la fonction AfficherIndividu(i)
(algorithme 2) et introduisons cette Section Critique dans la fonction
ModifierIndividu(i, num, rue, ville) (algorithme 5).
Résultat d’exécution :
Prenons le cas où un processus P1 modifie l’adresse de ind1 et un processus Q1 affiche
l’adresse de ind1 . La modification de l’adresse de ind1 par P1 est réalisée en exclu-
sion mutuelle. L’affichage de l’adresse de ind1 par Q1 est aussi réalisée en exclusion
mutuelle. Selon quel processus exécute la fonction entreeSC() en premier, l’adresse
affichée par Q1 sera différente mais cohérente.

30
Synchronisation entre processus (BH)

Algorithme 5 : Accès concurrents à une adresse en exclusion mutuelle


Variables :
ind1 : individu
SC : SectionCritique
Fonction AfficherIndividu(i :individu)
begin
// affiche l’adresse de l’individu i
entreeSC(SC)
afficher(i..numero)
afficher([Link])
afficher([Link])
sortieSC(SC)
end
Fonction ModifierIndividu(i :individu, num : entier, rue : chaine, ville : chaine)
begin
// modifie l’adresse de l’individu i
entreeSC(SC)
[Link] ← num
[Link] ← rue
[Link] ← ville
sortieSC(SC)
end
Algorithme de chaque Processus Pi
begin
...
ModifierIndividu(ind1 )
...
end
Algorithme de chaque Processus Qi
begin
...
AfficherIndividu(ind1 )
...
end

31
Synchronisation entre processus (BH)

Hypothèses et conditions pour l’exclusion mutuelle

La mise en place de l’exclusion mutuelle repose sur les hypothèses et conditions


suivantes :

1. Hypothèses pour l’exclusion mutuelle :


— les vitesses relatives des processus sont quelconques,
— les priorités ou les droits des processus sont quelconques,
— tout processus sort de sa section critique au bout d’un temps fini, il n’y a
pas de panne ni de blocage perpétuel en section critique.
2. Conditions pour l’exclusion mutuelle :
— deux processus ne doivent pas se trouver simultanément dans leur Section
Critique,
— aucun processus s’exécutant à l’extérieur de sa Section Critique ne doit blo-
quer d’autres processus,
— aucun processus ne doit attendre indéfiniment pour entrer en Section Cri-
tique (situation de famine),

Propriétés des algorithmes d’exclusion mutuelle :

Nous proposons par la suite des solutions d’implantation de l’Exclusion Mutuelle,


c’est à dire des fonctions d’entrée et de sortie de Section Critique. Les algorithmes
présentés doivent satisfaire les propriétés suivantes :
— sûreté : propriété qui garantit que rien de “mauvais” ne peut se produire. Dans
le cas de l’exclusion mutuelle, on doit s’assurer qu’à un moment donné, un seul
processus est en Section Critique.
— vivacité : propriété qui garantit que quelque chose de “bon” finira par se pro-
duire. Dans le cas de l’exclusion mutuelle, un processus qui a demandé la Sec-
tion Critique y entrera au bout d’un temps fini.

2.1.3 Solutions pour réaliser l’exclusion mutuelle

Deux grandes classes de solutions sont proposées en fonction de l’implantation de


la mise en attente des processus :
— Attente active : le processus est en attente de section critique mais consomme
du CPU ;
— Attente passive : le processus est en attente de section critique mais ne
consomme pas de CPU.
Dans la section suivante, nous présentons les solutions basées sur l’attente active.
Ces solutions ne seront pas retenues dans la suite du cours. Il n’est donc pas nécessaire
de les approfondir mais de porter votre intérêt sur les solutions basées sur l’attente
passive.

32
Synchronisation entre processus (BH)

2.2 Attente active

Dans cette section, nous présentons les solutions proposées pour réaliser l’exclu-
sion mutuelle avec attente active. Dans le cas de l’attente active, les processus en at-
tente de Section Critique ne sont pas endormis. Deux types de solutions sont présen-
tées : des solutions logicielles et des solutions matérielles.

2.2.1 Attente active : les solutions logicielles

Le verrou

L’idée la plus simple qui vient pour implanter l’exclusion mutuelle est d’utiliser
une variable partagée que nous nommerons occupée. Cette variable est un booléen.
Quand elle est positionnée à VRAI, cela indique que la Section Critique est occupée
et qu’un processus qui veut y accéder doit attendre par une itération que le booléen
passe à FAUX. Si le booléen est positionné à FAUX, le processus peut entrer en Section
Critique : il positionne alors le booléen à VRAI. Le problème est ici qu’un processus
peut lire le booléen à FAUX, alors qu’un autre processus l’a déjà fait (et pas encore posi-
tionné à VRAI) et ces processus peuvent ainsi entrer tous les deux en Section Critique.
Cela vient du fait que la lecture et l’écriture du booléen sont des opérations atomiques,
mais pas la séquence, lecture suivie de l’écriture. Il peut y avoir entrelacement des exé-
cutions.

L’alternance stricte

Soient deux processus P1 et P2 qui partagent une variable commune nommée


Tour, qui peut prendre les valeurs 1 et 2. La fonction d’entrée en Section Critique
consulte la valeur de Tour. L’exécution de la Section Critique de Pi n’est autorisée
que si la variable Tour à la valeur i. Lorsque le processus a terminé son travail en
Section Critique, il exécute la fonction de sortie de Section Critique. Celle-ci attribue
le numéro de l’autre processus à la variable Tour. Ce processus peut alors entrer en
Section Critique et empêche tous les autres processus d’y rentrer tant qu’il n’y est pas
lui-même sorti. Ainsi, la Section Critique est attribuée alternativement aux processus,
même s’ils ne l’ont pas demandée.
Cette solution convient lorsque les processus ont besoin alternativement de la Sec-
tion Critique.

Exercice 1 :
Quelle condition n’est pas garantie dans ce cas pour l’exclusion mutuelle ?

Correction 2.7

33
Synchronisation entre processus (BH)

L’algorithme de Peterson

En 1981, Peterson propose une solution logicielle à l’exclusion mutuelle. L’algo-


rithme 6 la présente appliquée au cas de deux processus P0 et P1 .
Par convention, dans les algorithmes de ce chapitre, les “Data” sont des variables
partagées et les arguments de chaque fonction sont des données propres à chaque
processus.

Algorithme 6 : Algorithme de Peterson


Variables
N : entier, nombre de processus, initialisé à 2
tour : entier, processus qui a le tour d’entrer en SC
interesse[N ] : tableau de booléens, initialisé à FAUX pour chaque processus
Fonction EntreeSC(numP rocess : entier)
autre : entier
begin
autre ← 1 − numP rocess
interesse[numP rocess] ← V RAI
tour ← numP rocess
while (tour = numProcess ET interesse[autre] = VRAI) do Attendre()
end
Fonction SortieSC(numP rocess : entier)
begin
interesse[numP rocess] ← FAUX
end

Exercice 2 : Déroulement algorithme de Peterson


Dérouler l’algorithme de Peterson avec deux processus P0 et P1 .
Tester les deux cas de figure suivants :
— P0 demande la Section Critique, puis une fois que P0 est en Section Critique, P1
demande à y entrer.
— P0 et P1 demandent la Section Critique en même temps.

Correction 2.7

L’algorithme de Lamport

Lamport propose une solution à base d’estampilles. Nous ne développons pas


cette solution, par contre nous présenterons la solution de Lamport concernant l’ex-
clusion mutuelle en systèmes distribués dans la seconde partie du cours.

34
Synchronisation entre processus (BH)

2.2.2 Attente active : les solutions matérielles

Ces solutions physiques sont supportées directement par le processeur.

Le masquage des interruptions

Le masquage (ou désactivation) des interruptions est une solution simple pour
réaliser l’exclusion mutuelle. En effet, si un processus qui entre en Section Critique
masque les interruptions aucun autre processus ne pourra y entrer en même temps
(sur un même processeur). Si les interruptions sont masquées, le processus qui est en
section critique ne peut pas être interrompu (soit par le scheduler, soit par un pro-
cessus plus prioritaire, par exemple), il peut alors examiner et actualiser la mémoire
partagée sans craindre l’intervention d’un autre processus.
Mais cette solution présente un certain nombre d’inconvénients :
— le masquage des interruptions ne peut être fait qu’en mode maître, donc par
le noyau. Il faut prévoir un appel système spécifique. Mais il est dangereux
de permettre aux utilisateurs de pouvoir masquer les interruptions (et s’ils ou-
bliaient de les réactiver à la sortie de la Section Critique ?)
— dans la Section Critique, l’utilisateur pourrait avoir besoin des interruptions
pour faire des entrées/sorties par exemple.
— si les Sections Critiques sont longues, cela peut retarder l’horloge du système.
En conclusion, le masquage/démasquage des interruptions est utilisé pour des
Sections Critiques très courtes dans le noyau et en particulier pour celles qui servent à
coder des mécanismes de synchronisation pour les programmes utilisateurs, comme
les sémaphores ou la synchronisation de base des langages comme Java. Mais cette
solution n’est pas appropriée en tant que mécanisme d’exclusion mutuelle pour les
processus utilisateurs.

L’instruction Test and Set

Certains processeurs offrent l’instruction élémentaire Test and Set (TAS ou TS)
exécutée par le matériel. Cette instruction permet de lire et d’écrire le contenu d’un
mot de la mémoire de façon indivisible. On peut représenter cette instruction avec le
pseudo-code donné dans l’algorithme 7 (comme elle est implantée dans le matériel, il
est possible de la rendre atomique).

Algorithme 7 : Instruction Test and Set


Fonction TS(a : registre, mot : entier)
begin
a ← mot
mot ← 1
end

35
Synchronisation entre processus (BH)

Cette instruction a deux opérandes : un registre a et une donnée (partageable) mot


de la mémoire centrale. Elle copie la donnée de la mémoire centrale dans le registre et
place la valeur 1 dans la donnée de la mémoire.
Cette instruction est toujours exécutée de façon indivisible ou atomique.
En utilisant cette instruction, il est possible de réaliser une exclusion mutuelle à
l’aide d’un verrou (voir algorithme 8). Nous avions tenté d’utiliser un verrou au début
de cette section, mais cela ne fonctionnait pas car nous ne possédions pas d’instruction
atomique pour réaliser une lecture suivie d’une écriture.

Algorithme 8 : Exclusion mutuelle avec TS


Fonction EntreeSC()
begin
TS(varSC, verrou)
while varSC = 1 do TS(varSC, verrou)
end
Fonction SortieSC()
begin
verrou ← 0
end

Deux processus partagent une variable verrou initialisée à 0. Quand un processus


P demande l’entrée en Section Critique, il exécute la fonction EntreeSC(). Si varSC
vaut 1, alors le verrou est positionné (P ne peut pas entrer en SC), sinon (varSC vaut
0) P entre en SC .
Cet algorithme a l’avantage d’être valable pour un certain nombre de processus,
mais ce n’est pas un algorithme équitable puisqu’un processus très rapide qui vient
de sortir de section critique peut y entrer de nouveau avant tous les autres.

2.2.3 Conclusion sur l’attente active

Toutes les solutions que nous venons de présenter ont pour inconvénient de faire
appel à l’attente active : quand un processus ne peut pas entrer en Section Critique,
il exécute une boucle pour attendre la permission d’y entrer. Dans ce cas, le proces-
seur est monopolisé par une simple attente. De plus, si des processus de priorités
différentes accèdent à une même Section Critique, on peut arriver à des situations de
blocage. Supposons que P2 soit plus prioritaire que P1 . P1 est en Section Critique, P2
veut entrer en Section Critique, il se met en attente car elle est déjà occupée. Comme
P2 a une priorité supérieure, le processeur ne passe pas la main à P1 qui ne peut jamais
sortir de Section Critique et P2 boucle en attente.

36
Synchronisation entre processus (BH)

2.3 Attente passive : les sémaphores

Ils constituent un outil élémentaire de synchronisation qui évite l’attente active. .


En effet, avec l’utilisation de sémaphores, les processus en attente de Section Critique
sont endormis. On parle donc ici d’attente passive. Ils permettent de mettre en place
facilement des Sections Critiques. Ils ont été introduits par Dijkstra en 1965.

2.3.1 Définition

Un sémaphore est une variable entière qui est accessible à partir de trois opéra-
tions :
— Init(S, valeur) : permet d’initialiser le sémaphore S avec valeur,
— P(S) : (du néerlandais Proberen signifie en français ”Puis-je ?” ou ”Prendre”), le
processus qui l’exécute est en attente (endormi) jusqu’à ce qu’une ressource soit
disponnible. Le processus en attente est déposé dans une file d’attente gérée par
le sémaphore.
— V(S) : (du néerlandais Verhogen signifie en français ” Vas-y” ou ”Libérer”),
cette opération rend une ressource disponible après que le processus a terminé
de l’exécuter. Le premier processus (s’il y en a un) de la file d’attente est libéré.
Une variable sémaphore est partagée par plusieurs processus devant gérer des
concurrences d’accès.

L’opération P(S)

Elle correspond à une tentative de franchissement. Le sémaphore est décrémenté.


S’il est négatif, le processus est bloqué et mis dans la file d’attente f(S), sinon il poursuit
son exécution.
Un processus P exécutant la primitive P(S), exécute le code donné dans l’algo-
rithme 9.

Algorithme 9 : Primitive P(S)


Primitive P(S)
begin
S ← S- 1
if S < 0 then
Entrer P dans file d’attente f(S)
Processus P bloqué
end if
end

37
Synchronisation entre processus (BH)

La primitive V(S)

Elle rend une autorisation. Si S est négatif alors un processus en attente dans la file
f(S) est réveillé. S est incrémenté. Un processus P exécutant la primitive V(S), exécute
le code donné dans l’algorithme 10.

Algorithme 10 : Primitive V(S)


Primitive V(S)
begin
if S < 0 then
Sortir le premier processus Q de la file f(S)
Processus Q actif
end if
S ← S+ 1
end

2.3.2 Implantation des sémaphores

Toute implantation de sémaphores repose :


— sur l’atomicité des opérations P(S) et V(S), c’est à dire sur le fait que la suite
d’opérations les réalisant (constituant une section critique) est non interruptible
(pour éviter les conflits entre processus).
— sur l’existence d’un mécanisme de file d’attente, permettant de mémoriser les
demandes d’opérations P(S) non satisfaites et de réveiller les processus en at-
tente.
L’interface POSIX pthreads propose une implantation de sémaphores.

Résumé : Dans la suite de ce chapitre quand nous utiliserons des sémaphores, nous
supposerons qu’une implantation de ces sémaphores existe. Ainsi, les primitives P ,V
et Init sont supposées implantées ainsi que la gestion de la file d’attente associée à S.

⇒ Pour utiliser un sémaphore, il suffira de l’initialiser, puis d’appliquer les opérations


P et/ou V.

Nous trouvons deux grandes familles de cas d’utilisation des sémaphores :


— l’exclusion mutuelle
— la synchronisation d’exécution

38
Synchronisation entre processus (BH)

2.3.3 Utilisation des sémaphores pour l’Exclusion Mutuelle

On peut utiliser les sémaphores pour accéder à une Ressource Critique en exclu-
sion mutuelle. L’accès à la Ressource Critique constitue une Section Critique. Dans ce
cas, chaque processus qui veut accéder à la Section Critique doit suivre la règle donnée
dans l’algorithme 11.

Algorithme 11 : Exclusion mutuelle avec sémaphores


Variables
S : sémaphore initialisé à 1
Fonction EntreeSC()
begin
// Le processus est bloqué (mis en file d’attente) s’il y a
déjà un processus en SC
P(S)
end
Fonction SortieSC()
begin
// le sémaphore est incrémenté, s’il y a un processus dans la
file il est réveillé
V(S)
end

Contraintes pour réaliser l’Exclusion Mutuelle :


— on suppose qu’un processus qui est en Section Critique en sort au bout d’un
temps fini (pas de boucle infinie, de destruction de processus) ;
— on suppose que l’on passe toujours par les fonctions de contrôle EntreeSC() et
SortieSC() pour l’entrée et la sortie de Section Critique.
On supposera donc que lors de l’utilisation des sémaphores les deux conditions pré-
cédentes sont remplies.

Exercice 3 : Exclusion mutuelle avec sémaphores


Dans quel cas l’utilisation des sémaphores pour gérer l’exclusion mutuelle est-elle
équitable ?
Correction 2.7

Exemple : Partage d’écran avec Sémaphore Reprenons l’exemple du partage


d’écran par deux processus P1 et P2 . Pour garantir que l’affichage de l’adresse d’un
individu est réalisée en exclusion mutuelle, nous introduisons un sémaphore sem ini-
tialisé à 1. Ce sémaphore est partagé par tous les processus Pi et il garantit que les
adresses affichées par les processus Pi ne seront pas intercalées. En effet, l’accès à
l’écran (Ressource Critique) est réalisé en exclusion mutuelle.

39
Synchronisation entre processus (BH)

Algorithme 12 : Partage d’écran avec Sémaphore


Variables :
ind1 , ind2 : individu
sem : sémaphore initialisé à 1
Fonction AfficherIndividu(i : individu)
begin
// Pour afficher l’adresse, il faut s’assurer qu’aucun autre
// processus n’accède à l’écran
// La fonction P décrémente le sémaphore, s’il est négatif,
// le processus est bloqué et mis en file d’attente
P(sem)
afficher(i..numero)
afficher([Link])
afficher([Link])
// Une fois l’accès à l’écran terminé, appel de la fonction
// V pour libérer la ressource critique
// Si le sémaphore est négatif, sortie d’un processus de la
// file d’attente, réveil de ce processus et incrémentation
// du sémaphore
V(sem)
end
Algorithme de chaque Processus Pi
begin
...
AfficherIndividu(ind1 )
...
end

40
Synchronisation entre processus (BH)

Résultat d’exécution :
Si le processus P1 exécute en premier l’appel à P(sem), alors il accède à l’écran et
cela garantit qu’il pourra exécuter la suite de son code (jusqu’à l’appel V(sem)) en
exclusion mutuelle. Si lors de cette exécution, P2 fait appel à P(sem), il sera mis en
attente jusqu’à ce que P1 libère la Section Critique. Le même schéma s’applique si
c’est P2 qui exécute en premier l’appel à P(sem) (voir tableau 2.2). Ainsi, les adresses
seront affichées correctement quelques soient les exécutions entrelacées.

P1 P2 Sémaphore
sem f(sem)
1 {}
afficherInd(ind2 )
P(sem) 0 {}
afficher(ind2 .numero)
afficherInd(ind1 )
P(sem) -1 {P1 }
P1 en attente
afficher(ind2 .rue)
afficher(ind2 .ville)
V(sem) 0 {}
Réveil
afficher(ind1 .numero)
afficher(ind1 .rue)
afficher(ind1 .ville)
V (sem) 1 {}

TABLE 2.2 – Résultat d’exécution : Partage d’écran avec Sémaphore

Exemple : Accès concurrents à une adresse avec Sémaphores Reprenons l’exemple


de l’accès concurrent à une adresse par deux processus de la section précédente.
Pour garantir que ces accès soient réalisés en exclusion mutuelle, nous introdui-
sons un sémaphore sem initialisé à 1 (algorithme 13). Ce sémaphore est partagé par
tous les processus Pi et Qi . Ce sémaphore garantit que l’accès à une adresse se fait en
exclusion mutuelle.
Résultat d’exécution :
La modification de l’adresse de ind1 par P1 est réalisée en exclusion mutuelle. L’affi-
chage de l’adresse de ind1 par Q1 est aussi réalisée en exclusion mutuelle. Selon quel
processus exécute la fonction entreeSC() en premier, l’adresse affichée par Q1 sera dif-
férente mais cohérente. Le tableau 2.3 donne un résultat d’exécution.

41
Synchronisation entre processus (BH)

Algorithme 13 : Accès concurrents à une adresse avec sémaphore


Variables :
ind1 : individu
sem : sémaphore initialisé à 1
Fonction AfficherIndividu(i :individu)
begin
// affiche l’adresse de l’individu i
// l’accès à l’adresse se fait en exclusion mutuelle
// grâce au sémaphore sem
P(sem)
afficher(i..numero)
afficher([Link])
afficher([Link])
V(sem)
end
Fonction ModifierIndividu(i :individu, num : entier, rue : chaine, ville : chaine)
begin
// modifie l’adresse de l’individu i
// l’accès à l’adresse se fait en exclusion mutuelle
// grâce au sémaphore sem
P(sem)
[Link] ← num
[Link] ← rue
[Link] ← ville
V(sem)
end
Algorithme de chaque Processus Pi
begin
...
ModifierIndividu(ind1 ,numero,rue,ville)
...
end
Algorithme de chaque Processus Qi
begin
...
AfficherIndividu(ind1 )
...
end

42
Synchronisation entre processus (BH)

P1 Q1 Sémaphore
sem f(sem)
1 {}
afficherInd(ind1 )
P(sem) 0 {}
afficher(ind1 .numero)
Modifier(ind1 ,numero,rue,ville)
P(sem) -1 {P1 }
P1 en attente
afficher(ind1 .rue)
afficher(ind1 .ville)
V(sem) 0 {}
Réveil
ind1 .numero ← numero
ind1 .rue ← rue
ind1 .ville ← ville
V(sem) 1 {}

TABLE 2.3 – Résultat d’exécution : Accès concurrent à une adresse avec sémaphore

Exercice 4 : Exclusion mutuelle avec sémaphores : accès concurrent à une cuisine


Dans un immeuble collectif, trois cuisinières partagent une seule cuisine pour préparer
les repas. Cette cuisine étant très petite, une seule cuisinière a accès à la cuisine à la
fois. Nous nous proposons d’écrire un algorithme simulant l’accès à cette cuisine par
les cuisinières. Chacune des cuisinières Ci exécutera la fonction EntreeCuisine()
lorsqu’elle voudra accéder à la cuisine et la fonction SortieCuisine() lorsqu’elle
quittera la cuisine. Ces fonctions sont données dans l’algorithme 14.
Si les cuisinières exécutent les fonctions données dans l’algorithme 14, il n’y a pas
de garantie pour que l’accès à la cuisine se fasse en exclusion mutuelle.
Compléter les fonctions EntreeCuisine() et SortieCuisine() de l’algo-
rithme 14 pour garantir que l’accès de la cuisine se fait en exclusion mutuelle en
utilisant des sémaphores. Donner un exemple d’exécution avec 3 cuisinières voulant
accéder à la cuisine : C2 , C1 , puis C3 .

Correction 14

43
Synchronisation entre processus (BH)

Algorithme 14 : Algorithme : partage d’une cuisine


Variables
Fonction EntreeCuisine()
begin
// la cuisinière entre dans la cuisine
end
Fonction SortieCuisine()
begin
// la cuisinière sort de la cuisine,
end
Algorithme de chaque cuisinière Ci
begin
EntreeCuisine()
cuisiner()

SortieCuisine()
end

Extension de l’exclusion mutuelle : accès à une Ressource Critique disponible en


plusieurs exemplaires Supposons qu’il existe k exemplaires d’une même Ressource
Critique (k≥ 2). L’accès à un exemplaire de la ressource constitue une Section Cri-
tique. À un instant donné, il peut y avoir autant de processus en Section Critique que
d’exemplaires initialement disponibles, k dans notre exemple. Pour synchroniser l’ac-
cès à cette Ressource Critique, on utilise un sémaphore S initialisé à k (représentant à
chaque instant le nombre d’exemplaires de la ressource disponible). Chaque proces-
sus voulant accéder à l’une des ressources doit exécuter les primitives données dans
l’algorithme 15.
Cette problématique est aussi appelée cohorte et lot de ressources banalisées.

Algorithme 15 : Exclusion mutuelle avec sémaphores : accès à une ressource dis-


ponible en k exemplaires
Variables
S : sémaphore initialisé à k
Fonction EntreeSC()
begin
P(S)
end
Fonction SortieSC()
begin
V(S)
end

44
Synchronisation entre processus (BH)

Exercice 5 : Exclusion mutuelle sur une ressource critique disponible en plusieurs


exemplaires à l’aide de sémaphores : partage de 2 poêles

Dans la cuisine d’un restaurant, il y a seulement 2 grandes poêles. Plusieurs cui-


siniers ne peuvent pas utiliser une même poêle à la fois. Chacun des cuisiniers Ci
exécute l’algorithme donné dans 16.
Compléter le pseudo-code donné dans l’algorithme 16 pour résoudre le problème
de l’accès à ces différentes poêles à l’aide de sémaphore. Donner un résultat d’exécu-
tion avec 3 cuisiniers qui utilisent les 2 poeles : C2 , C1 , puis C3 ont besoin d’une poêle
pour cuisiner. (Correction 2.7)

Algorithme 16 : Algorithme : partage de 2 poêles


Variables
Fonction PrendrePoele()
begin
Prend une poêle
end
Fonction RendrePoele()
begin
rend une poêle
end
Algorithme exécuté par chaque cuisinier Ci
begin
...
PrendrePoele()
utiliser la poêle
RendrePoele()
...
end

45
Synchronisation entre processus (BH)

2.3.4 Utilisation des sémaphores pour la Synchronisation d’Exécution


(Rendez-vous)

Synchronisation d’exécution entre deux processus Un processus P1 doit attendre


qu’un processus P2 soit arrivé à un certain stade de son exécution, pour continuer (ou
commencer) son exécution.
Pour résoudre ce problème, on définit un sémaphore S initialisé à 0. Ainsi, le pro-
cessus P1 exécutant P(S) est bloqué tant que le processus P2 n’a pas exécuté V(S).
P1 et P2 exécutent respectivement les fonctions codeP1() et codeP2() données
dans l’algorithme 17.

Algorithme 17 : Synchronisation d’exécution entre 2 processus avec sémaphore


Variables
S : sémaphore initialisé à 0
Fonction codeP1 ()
begin
// P1 est en attente sur S (initialisé à 0)
P(S)
travailApresSynchro()
end
Fonction codeP2 ()
begin
travailAvantSynchro()
// P2 arrive au point de rendez-vous : il réveille P1
V(S)
end

Un résultat d’exécution de l’algorithme 17 est donné dans le tableau 2.4.

P1 P2 Sémaphore
S f(S)
.... ...
P(S) -1 {P1 }
P1 en attente

V(S) 0 {}
P1 réveillé

TABLE 2.4 – Résultat d’exécution : Synchronisation d’exécution entre deux processus


avec sémaphore

46
Synchronisation entre processus (BH)

Rendez-vous d’un processus avec plusieurs autres processus C’est un cas particu-
lier de la synchronisation d’exécution. Un processus P se synchronise avec n autres
processus Qi . Il poursuit son exécution lorsque les Qi sont parvenus à un point de
rendez-vous. Les n processus Qi poursuivent leurs exécutions lorsque tout le monde
a atteint le point de rendez-vous.

Exercice 6 : Synchronisation d’exécution : Le rendez-vous d’un processus avec plu-


sieurs autres processus à l’aide de sémaphore
Écrire l’algorithme exécuté par le processus P et celui exécuté par chaque processus
Qi .

Correction 2.7

2.3.5 Les mutex

Définition Un mutex est une version simplifiée de sémaphore.


Il a deux états possibles :
— verrouillé : le mutex vaut 0
— déverrouillé : le mutex vaut 1
Comme pour les sémaphores, une file d’attente est associée au mutex.
Un mutex est accédé avec trois fonctions :
— mutexInit(m, val) : initialisation du mutex, val prend la valeur 0 ou 1.
— mutexLock(m) ou prendreMutex(m) : invoquée par un processus qui veut
accéder à la Section Critique. Si le mutex est égal à 1, il passe à 0 et le processus
poursuit son exécution sinon, le processus est bloqué et mis en file d’attente.
— mutexUnlock(m) ou libererMutex(m) : invoquée par un processus qui li-
bère la Section Critique. Le mutex passe à 1. Si la file d’attente associée au mutex
est non vide, un processus est sorti de la file d’attente et le mutex passe à 0.

Implantation des mutex Toute implantation de mutex repose :


— sur l’atomicité des opérations mutexLock(m) et mutexUnlock(m), c’est à
dire sur le fait que la suite d’opérations les réalisant (constituant une section
critique) est non interruptible (pour éviter les conflits entre processus).
— sur l’existence d’un mécanisme de file d’attente, permettant de mémoriser les
demandes d’opérations mutexLock(m) non satisfaites et de réveiller les pro-
cessus en attente.
Il est facile d’implémenter les mutex si une instruction TS (Test and Set) est offerte
par le matériel.

47
Synchronisation entre processus (BH)

Résumé Dans la suite de ce chapitre quand nous utiliserons des mutex, nous suppo-
serons qu’une implantation de ces mutex existe. Ainsi, les primitives mutexLock(m),
mutexUnlock(m) et mutexInit(m, val) sont supposées implantées ainsi que la
gestion de la file d’attente associée au mutex.

Cas d’utilisation des mutex Comme les sémaphores, les mutex peuvent être utilisés
pour réaliser de l’exclusion mutuelle et de la synchronisation d’exécution.
Mais, attention, ils ne peuvent pas être utilisés pour réaliser de l’exclusion mu-
tuelle sur une ressource disponible en plusieurs exemplaires, ni pour réaliser des
rendez-vous entre plusieurs processus. En effet, un mutex ne peut prendre que deux
valeurs : 0 ou 1.

Exercice 7 : Exclusion mutuelle avec mutex : accès concurrent à une cuisine


Reprendre l’exemple de l’accès concurrent à une cuisine résolu précédemment avec
des sémaphores et le résoudre avec des mutexs. Donner un résultat d’exécution avec
trois cuisinières.

Correction 2.7

48
Synchronisation entre processus (BH)

2.4 Attente passive : les moniteurs

2.4.1 Définition

Les moniteurs proposent une solution de haut niveau pour la protection des don-
nées partagées, ils ont été introduits par Hoare en 1974.
Un moniteur contient au moins une variable de type condition qui est représen-
tée par une file d’attente de processus. Cette variable condition possède un iden-
tificateur mais n’a pas de valeur. Une condition est uniquement manipulée par les
primitives wait et signal :
— la primitive wait : bloque le processus appelant sur la condition associée et
libère l’accès au moniteur.
— la primitive signal : réveille un des processus en attente sur la condition
associée à l’intérieur du moniteur (premier de la file d’attente). Si aucun pro-
cessus n’est en attente, la primitive signal ne fait rien (ce qui diffère des séma-
phores où l’entier associé permet de conserver toutes les actions des primitives
P et V).
Le processus réveillé reprend son exécution à l’instruction qui suit l’appel à la
primitive wait qui l’a bloqué. Avec les moniteurs de Hoare, on considère que
le processus réveillé a la priorité d’accès au moniteur lorsque le processus qui a
fait appel à la primitive signal libère le moniteur (ce qui n’est pas forcément
garanti dans les différentes implantations des moniteurs).
Chaque moniteur possède :
— des données internes ou variables d’états
— des primitives d’accès ou points d’entrée
— des primitives internes
— une ou plusieurs conditions (à chacune est associée une file d’attente)

A un moment donné, seul un processus peut être actif dans le moniteur. Ainsi, tant
qu’il y a un processus actif dans le moniteur, toute demande d’entrée dans le moniteur
ou d’exécution d’une de ses primitives sera bloquée . L’accès au moniteur est donc
réalisé en exclusion mutuelle.

2.4.2 Implantation

Dans le cours, lors de l’utilisation de moniteurs, nous supposons qu’il existe un


type condition (file d’attente gérée) et que les primitives wait et signal sont im-
plantées.
Les moniteurs sont implantés dans différents langages comme Java, et les langages
de la norme Posix.

49
Synchronisation entre processus (BH)

2.4.3 Utilisation des moniteurs pour l’Exclusion Mutuelle

Les moniteurs peuvent être utilisés pour protéger l’accès à une Ressource Cri-
tique en Exclusion Mutuelle. L’accès à la Ressource Critique constitue une Section
Critique. Son accès en exclusion mutuelle est gérée à l’aide d’un moniteur. Un type
TSectionCritiqueMoniteur est définit (algorithme 18).
Une variable commune aux processus voulant accéder la SC, de type
TSectionCritiqueMoniteur, est déclarée (sectionCritiqueMon).
Chaque processus voulant accéder à la Section Critique le fera avec l’appel
[Link](). A la sortie de la Section Critique, il ap-
pellera la fonction [Link]() qui libère la ressource
critique et permet à un autre processus en attente d’y accéder.

50
Synchronisation entre processus (BH)

Algorithme 18 : Exclusion mutuelle avec moniteurs


Type TSectionCritiqueMoniteur Moniteur
begin
Variables
occupe : booleen // booléen indiquant si la SC est occupée
accesSC : condition // condition sur laquelle les processus sont mis en attente d’entrée
en SC
Fonction EntreeSC-Mon()
begin
// Si la SC critique est occupée, mise en attente sur la condition accessSC
if occupe == vrai then
[Link]()
end if
// Le processus accède à la SC, maj du booléen occupe
occupe = vrai
end
Fonction SortieSC-Mon()
begin
// A la sortie de SC, maj du booléen occupé et réveil d’un processus en attente s’il y en a
un
occupe = faux
[Link]()
end
Fonction Initialisation()
begin
occupe = faux
end
end
Variables
sectionCritiqueMon : TSectionCritiqueMoniteur // déclaration d’une variable de type
TSectionCritiqueMoniteur
entreeSC()
begin
[Link]-Mon()
end
sortieSC()
begin
[Link]-Mon()
end
begin
// Code exécuté par chaque processus P voulant accéder à la Section Critique ;
entreeSC()
Travail en SC
sortieSC()
end

51
Synchronisation entre processus (BH)

Exercice 8 : Exclusion mutuelle avec moniteurs : accès concurrent à une cuisine

Dans un immeuble collectif, trois cuisinières partagent une seule cuisine pour pré-
parer les repas. Cette cuisine étant très petite, une seule cuisinière a accès à la cuisine
à la fois.
Reprendre l’algorithme 14 et résoudre l’accès à la cuisine en exclusion mutuelle à
l’aide d’un moniteur.

Exercice 9 : Exclusion mutuelle sur une ressource critique disponible en plusieurs


exemplaires à l’aide de moniteurs

Dans la cuisine d’un restaurant, il y a seulement 3 grandes poêles. Plusieurs cuisi-


niers ne peuvent pas utiliser une même poêle à la fois. Résoudre le problème de l’accès
à ces différentes poêles à l’aide de moniteurs en complétant l’algorithme 16.

2.4.4 Utilisation des moniteurs pour la synchronisation d’exécution ou


rendez-vous

Rendez-vous entre deux processus Un processus P1 doit attendre qu’un processus


P2 soit arrivé à un certain stade de son exécution, pour continuer (ou commencer) son
exécution.
Pour résoudre ce problème, on définit un moniteur RendezVous suivant l’al-
gorithme 19. Chaque processus devra appeler la fonction Arriver() du moniteur
quand il atteindra le point de rendez-vous.

Rendez-vous d’un processus avec plusieurs autres processus C’est un cas particu-
lier de la synchronisation d’exécution. Un processus P attend que n autres processus
Qi soient parvenus à un point de rendez-vous pour poursuivre son exécution. Les n
processus Qi poursuivent leurs exécutions lorsque tout le monde a atteint le point de
rendez-vous.

Exercice 10 : Synchronisation d’exécution : Le rendez-vous d’un processus avec plu-


sieurs autres processus à l’aide de moniteurs
Écrire l’algorithme exécuté par le processus P et celui exécuté par chaque processus
Qi à l’aide de moniteurs.

52
Synchronisation entre processus (BH)

Algorithme 19 : Rendez-vous entre deux processus avec moniteurs


Type TRendezVousMoniteur Moniteur
begin
Variables
unProcess : booleen // booléen indiquant si un process est déjà au point de
rendez-vous
processArrive : condition // condition sur laquelle le premier process se met en
attente
Fonction Arriver()
begin
if unProcess == vrai then
// Si un process est déjà au point de rendez-vous : il est réveillé, le rendez-vous a
lieu
[Link]()
else
// Sinon, maj du booléen indiquant qu’un process est au point de rendez-vous et
mise en attente de ce process sur la condition processArrive
unProcess = vrai
[Link]()
end if
end
Fonction Initialisation()
begin
// aucun processus n’est encore arrivé au point de rendez-vous
unProcess = faux
end
end
Variables
rendezVousMon : TRendezVousMoniteur // déclaration d’une variable de type
TRendezVousMoniteur
Fonction codeP1 ()
begin
...
// P1 est au point de rendez-vous, il est mis en attente si P2 n’est pas arrivé. Continue son
exécution sinon
[Link]()
...
end
Fonction codeP2 ()
begin
...
// P2 arrive au point de rendez-vous, il est mis en attente si P1 n’est pas arrivé. Continue
son exécution sinon
[Link]()
...
end

53
Synchronisation entre processus (BH)

2.5 Les problèmes d’interblocage

Un ensemble de processus est en interblocage si et seulement si tout processus de


l’ensemble est en attente d’une ressource qui ne peut être libérée que par un autre
processus de l’ensemble.
L’utilisation de sémaphores, de mutexs ou de moniteurs peut amener à des si-
tuations d’interblocage entre processus, c’est à dire à des situations où les processus
“s’attendent” mutuellement.
L’algorithme 20 illustre un exemple où, selon l’entrelacement des exécutions, nous
pouvons arriver à une situation d’interblocage entre les deux processus R et Q qui uti-
lisent deux sémaphores semA et semB d’exclusion mutuelle (tableau 2.5). Au début de
l’exemple, les sémaphores sont initialisés à 1. Le processus Q réalisant P(semA) peut
passer la barrière du sémaphore semA. Ensuite le processus R exécute P(semB), il
peut passer la barrière du sémaphore semB. Par contre, les processus sont ensuite blo-
qués car le processus Q attend semB détenu par le processus R et le processus R attend
semA détenu par le processus Q. Ces sémaphores ne pourront jamais être rendus.

Algorithme 20 : Exemple d’interblocage


Variables
semA, semB : sémaphores initialisé à 1
Code exécuté par le processus Q :
begin
...
P(SemA)
P(SemB)
Section Critique
V(SemB)
V(SemA)
...
end
Code exécuté par le processus R :
begin
...
P(SemB)
P(SemA)
Section Critique
V(SemA)
V(SemB)
...
end

54
Synchronisation entre processus (BH)

Q R Sémaphores
semA f(semA) semB f(semB)
1 {} 1 {}
P(semA) 0 {} 1 {}
Q continue exec
P(semB) 0 {} 0 {}
R continue exec
P(semB) 0 {} -1 {Q}
Q bloqué
P(semA) -1 {R} -1 {Q}
R bloqué

TABLE 2.5 – Résultat d’exécution : interblocage

2.6 Les problèmes classiques de synchronisation

Les problèmes classiques de synchronisation sont employés pour tester chaque


nouveau schéma de synchronisation.

2.6.1 Producteur-Consommateur

Ce problème est aussi appelé problème du tampon délimité ou bounded buffer.


Deux processus partagent un tampon commun de taille fixe. L’un deux, le
Producteur, place des informations dans le tampon. L’autre, le Consommateur, lit
ces informations. Deux types de problèmes de synchronisation doivent être gérés :
— le producteur ne doit pas écrire dans un tampon plein,
— le consommateur ne doit pas lire des informations dans un tampon vide.

Producteur-Consommateur : une solution à base de sémaphores

L’algorithme 21 et 22 présente les algorithmes exécutés respectivement par les pro-


cessus Producteur et Consommateur qui accèdent à un même tampon de taille N .
Le producteur n’écrit pas dans un tampon plein car il appelle la fonction
P(semVide) avant toute écriture (le sémaphore est initialisé à N et décrémenté à
chaque écriture). Lorsque le consommateur a consommé une information, il libère une
place dans le tampon (grâce à l’appel à V(semVide)).
Le consommateur ne lit pas dans le tampon vide car il appelle la fonction
P(semPlein) avant toute consommation (ce sémaphore est initialisé à 0). Lorsque
le producteur produit une information, il incrémente ce sémaphore par l’appel
V(semPlein).
Dans cette solution :

55
Synchronisation entre processus (BH)

— on n’utilise pas de variables communes pour gérer les indices du tampon. Si tel
était le cas, il ne faudrait pas oublier de gérer leurs accès en exclusion mutuelle,
à l’aide de mutexs par exemple.
— on considère que consommateur et producteur peuvent accéder en même
temps au tampon. Si l’on voulait que l’accès soit réalisé en exclusion mutuelle,
il faudrait ajouter un mutex qui serait pris à chaque accès par le producteur et
le consommateur.

56
Synchronisation entre processus (BH)

Algorithme 21 : Producteur-Consommateur avec sémaphores


Variables
// sémaphore gérant le nombre d’emplacements occupés du tampon
semP lein : sémaphore
// sémaphore gérant le nombre d’emplacements vides du tampon
SemV ide : sémaphore
// emplacement du tampon où le producteur écrit
tete : entier
//emplacement du tampon où le consommateur lit
queue : entier
tampon[N ] : tableau de N éléments
Fonction Initialisation
begin
Init(SemPlein,0) // aucun emplacement occupé à l’init
Init(SemVide,N) // tous les emplacements sont vides à l’init
tete ← 0
queue ← 0
end
Fonction Deposer(X)
begin
// Le producteur est en attente s’il n’y a pas d’emplacement
vide
P(semV ide)
// dépôt de X dans le tampon
tampon[tete] ← X
// mise à jour de l’emplacement d’écriture
tete ← (tete + 1) mod N
// un emplacement est rempli, réveil du consommateur s’il est
en attente
V(semP lein)
end
int Fonction Retirer()
Variables
Y : entier
begin
// le consommateur est en attente s’il n’y a aucun
emplacement rempli
P(semP lein)
// lecture de l’élément du tampon
Y ← tampon[queue]
// mise à jour de l’emplacement de lecture
queue ← (queue + 1) mod N
// un emplacement est libéré, réveil du producteur s’il est
en attente
V(semV ide)
return(Y )
end

57
Synchronisation entre processus (BH)

Algorithme 22 : Producteur-Consommateur avec sémaphores (suite)


// Fonction exécutée par chaque Producteur P
begin
Variables
X : entier
while vrai do
produire(X)
Deposer(X)
end while
end
// Fonction exécutée par chaque Consommateur C
begin
Variables
X : entier
while vrai do
X = Retirer()
consommer(X)
end while
end

Exercice 11 : Généralisation à plusieurs Producteurs et Consommateurs avec séma-


phores
Plusieurs producteurs et consommateurs accèdent à un même tampon de taille n.
Écrire les algorithmes exécutés par chaque producteur Pi et chaque consommateur
Ci .
Correction 2.7

Producteur-Consommateur : une solution à base de moniteurs

L’algorithme 23 et 24 présente le moniteur ProducteurConsommateurs ainsi


que les algorithmes exécutés par les processus accédant à un même tampon de taille
N . Nous supposons que les fonctions depot() et retrait() sont déjà écrites (cf
algorithme à l’aide de sémaphores).

58
Synchronisation entre processus (BH)

Algorithme 23 : Producteur-Consommateur avec moniteurs


Type TProdConsMoniteur Moniteur
begin
Variables
compteur : entier // nombre d’emplacement occupé dans le tampon
Plein : condition // condition sur laquelle le producteur se met en attente si le tampon
est plein
Vide : condition // condition sur laquelle le consommateur se met en attente si le
tampon est vide

Fonction Deposer(X :entier)


begin
if compteur == N then
// Si le tampon est plein, le producteur est mis en attente
[Link]()
end if
// dépôt de X dans le tampon
depot(X)
// il y a un élément de plus dans le tampon
compteur=compteur+1
if compteur == 1 then
// Si le tampon était vide avant cet ajout, réveil du consommateur s’il est en attente
[Link]()
end if
end
Fonction Retirer(X :entier)
begin
if compteur == 0 then
// Si le tampon est vide, le consommateur est mis en attente
[Link]()
end if
// retrait de X du tampon
retrait(X)
// il y a un élément de moins dans le tampon
compteur=compteur-1
if compteur == N-1 then
// Si le tampon était plein avant ce retrait, réveil du producteur s’il est en attente
[Link]()
end if
end
Fonction Initialisation()
begin
// A l’init, le tampon est vide
compteur = 0
end
end

59
Synchronisation entre processus (BH)

Algorithme 24 : Producteur-Consommateur avec moniteurs (suite)


Variables
producteurConsommateurMon : TProdConsMoniteur // déclaration d’une variable de
type TProducteurConsommateurMoniteur
Processus Producteur
begin
X : entier
while vrai do
Produire(X)
[Link](X)
end while
end
Processus Consommateur
begin
X : entier
while vrai do
[Link](X)
Consommer(X)
end while
end

Exercice 12 : Généralisation à plusieurs Producteurs et Consommateurs avec moniteur


Plusieurs producteurs et consommateurs accèdent à un même tampon de taille N .
Quelles modifications faut il apporter à l’algorithme 23 ?

Producteur-Consommateur : Exemples d’utilisation On retrouve ce problème clas-


sique de synchronisation dans les cas suivants de coopération entre processus concur-
rents :
— Impression : des processus utilisateurs préparent des fichiers de texte et dé-
posent des requêtes au service d’impression. Ces requêtes sont traitées par l’un
des processus du service d’impression à un moment où une des imprimantes
est disponible.
— Entrée-Sortie : un processeur d’entrée-sortie reçoit des données externes, ca-
ractère par caractère et les stocke dans une zone de mémoire tampon. Quand
le tampon est plein, un processus spécialisé est alerté, il peut consommer les
données.
— Contrôle de flux : un processus d’un site émetteur dans un réseau envoie des
messages à destination d’un site récepteur. Celui-ci doit être prêt à recevoir ces
messages ; mais l’émetteur ne doit pas les envoyer plus vite que le récepteur ne
peut les recevoir.
— Service de messagerie : un abonné peut rédiger son courrier et le déposer dans
la boîte aux lettres du destinataire (que celui-ci soit connecté ou non), et un
abonné peut lire son courrier (que l’émetteur soit connecté ou non).

60
Synchronisation entre processus (BH)

2.6.2 Lecteurs-Rédacteurs

Un objet de données (comme un fichier ou un enregistrement) est partagé par plu-


sieurs processus concurrents. Certains de ces processus veulent lire l’objet partagé (les
Lecteurs) tandis que d’autres veulent pouvoir le modifier (les Rédacteurs). Plusieurs lec-
teurs peuvent lire l’objet en même temps, par contre un seul rédacteur peut accéder
à l’objet à un moment donné (pas d’autres rédacteurs, ni de lecteurs). Les rédacteurs
doivent donc avoir un accès exclusif à l’objet partagé.
Il existe plusieurs variations du problème des Lecteurs-Rédacteurs, chacune pre-
nant en compte des priorités spécifiques. Nous présentons ces différentes variations à
la suite.

Lecteurs-Rédacteurs : Priorité aux lecteurs

S’il existe des lecteurs sur l’objet partagé, toute nouvelle demande de lecture est
acceptée. Le risque de cette solution est la famine des rédacteurs : un rédacteur peut
ne jamais pouvoir accéder à l’objet partagé.

Lecteurs-Rédacteurs : Priorité aux lecteurs à base de sémaphores L’algorithme 25


propose une solution à base de sémaphores. Nous donnons ci-après une explication
de l’algorithme.

Le nombre de lecteurs concurrents est comptabilisé avec la variable (nbLect). Ce


nombre est incrémenté à chaque nouvelle entrée d’un lecteur et décrémenté à toute
sortie d’un lecteur. Les opérations sur ce nombre sont réalisées en exclusion mu-
tuelle entre lecteurs à l’aide du sémaphore semL. Le premier lecteur d’un groupe
de lecteurs concurrents doit demander l’exclusion mutuelle avec toute opération
d’écriture par un rédacteur : cela est réalisé à l’aide du sémaphore semE. Le der-
nier lecteur du groupe libère la section critique avec les rédacteurs (semE).
Les rédacteurs écrivent en exclusion mutuelle avec les autres rédacteurs et avec le
groupe de lecteurs à l’aide du sémaphore semE.
Comme nous l’avons déjà dit, cet algorithme n’est pas équitable : s’il n’y a jamais
de dernier lecteur dans un groupe, les lecteurs se coalisent et c’est la famine pour
les rédacteurs.

Exercice 13 : Algorithme Lecteurs-Rédacteurs : priorité aux lecteurs avec sémaphores


Dérouler l’algorithme 25 sur la séquence suivante : un lecteur L1 demande l’accès à la
Section Critique. Pendant que ce lecteur est en SC, un rédacteur R1 demande l’accès à
la SC, puis un lecteur L2 demande l’accès à la SC. Montrer que L2 entre en SC avant
R1 .

61
Synchronisation entre processus (BH)

Algorithme 25 : Algorithme Lecteurs-Rédacteurs : priorité aux lecteurs avec sé-


maphores
Variables
semE : sémaphore, initialisé à 1 // exclusion mutuelle en écriture
semL : sémaphore, initialisé à 1 //exclusion mutuelle entre lecteurs sur la variable
nbLect
nbLect : entier, initialisé à 0 //nombre de lecteurs en cours sur l’objet
Fonction debutLecture()
begin
P(semL)
nbLect ← nbLect + 1
if nbLect = 1 then P(semE)

V(semL)
end
Fonction finLecture()
begin
P(semL)
nbLect ← nbLect- 1
if nbLect = 0 then V(semE)

V(semL)
end
Fonction debutEcriture()
begin
P(semE)
end
Fonction finEcriture()
begin
V(semE)
end

62
Synchronisation entre processus (BH)

Lecteurs-Rédacteurs : Priorité aux lecteurs à base de moniteurs L’algorithme 26


et 27 propose une solution à base de moniteur. Un lecteur exécute la fonction
Lecture() quand il veut lire et un rédacteur la fonction Ecriture() quand il veut
écrire.

Lecteurs-Rédacteurs : Priorité aux lecteurs, sans famine des rédacteurs

S’il existe des lecteurs sur l’objet partagé, toute nouvelle demande de lecture est
acceptée sauf s’il y a un rédacteur en attente.

Lecteurs-Rédacteurs : Priorité aux rédacteurs

Un lecteur ne peut lire que si aucun rédacteur n’est présent ou en attente. Dans ce
cas, il peut y avoir famine des lecteurs.

Lecteurs-Rédacteurs : FIFO

Les demandes d’accès à l’objet sont servies dans l’ordre d’arrivée. S’il y a plusieurs
lecteurs consécutifs, ils sont servis ensemble. Dans ce cas, on profite moins du parallé-
lisme d’accès de plusieurs lecteurs qu’avec la solution donnant la priorité aux lecteurs,
surtout si les demandes d’accès alternent entre lecteurs et rédacteurs.

Lecteurs-Rédacteurs : Exemples d’utilisation

En général, le problème des Lecteurs-Rédacteurs permet de modéliser les accès aux


bases de données. Par exemple, un système de réservation de billets d’avion où plu-
sieurs processus sont en concurrence pour y effectuer des lectures/écritures. Plusieurs
processus peuvent consulter la base en même temps. Par contre, si un processus est en
train de l’actualiser, aucun autre processus ne peut y accéder, même pas en lecture.

63
Synchronisation entre processus (BH)

Algorithme 26 : Lecteurs-Rédacteurs : priorité aux lecteurs avec moniteurs (1)


Type TLecteurRedacteurMoniteur Moniteur
begin
Variables
nbLecteur : entier
ecriture : booleen
accordLecture condition
accordEcriture : condition
Fonction Initialisation()
begin
nbLecteur = 0
ecriture=faux
end
Fonction debutLecture()
begin
incrémentation du nombre de lecteurs
nbLecteur ++
S’il y a une ecriture en cours, attente sur la condition accordLecture
if ecriture then
[Link]()
end if
réveil d’un lecteur, s’il y en a un en attente : plusieurs lecteurs peuvent accéder la SC en même
temps
[Link]()
end
Fonction finLecture()
begin
décrémentation du nombre de lecteurs
nbLecteur –
s’il n’y a plus de lecture en cours, réveil d’un rédacteur sur la condition accordEcriture
if nbLecteur == 0 then
[Link]()
end if
end
Fonction debutEcriture()
begin
S’il y a au moins une lecture en cours ou une écriture, attente sur la condition accordEcriture
if nbLecteur > 0 || ecriture then
[Link]()
end if
maj du booléen ecriture : une écriture est en cours
ecriture=vrai
end
Fonction finEcriture()
begin
maj du booléen ecriture : il n’y a plus d’écriture en cours
ecriture=faux
s’il y a un lecteur ou plus en attente, réveil de ce lecteur
if nbLecteur > 0 then
[Link]()
else
sinon réveil d’un rédacteur
[Link]()
end if
end 64
end
Synchronisation entre processus (BH)

Algorithme 27 : Lecteurs-Rédacteurs : priorité aux lecteurs avec moniteurs (2)


Variables
lecteurRedacteurMon : TLecteurRedacteurMoniteur
Fonction Lecture()
begin
[Link]()
Lire()
[Link]()
end
Fonction Ecriture()
begin
[Link]()
Ecrire()
[Link]()
end

2.6.3 Le repas des philosophes

Ce problème a été introduit par Dijkstra. Cinq philosophes sont assis autour d’une
table ronde. Chacun a devant lui une assiette de riz qui n’est jamais vide. Chaque
philosophe a besoin de deux baguettes pour manger. Entre chaque assiette se trouve
une baguette. On suppose que les philosophes ont deux activités : manger et penser.
Quand un philosophe a faim, il essaie de prendre deux baguettes, une à gauche de son
assiette et une à droite, sans ordre défini. S’il y arrive, il mange et quand il n’a plus faim
il repose les baguettes et se met à penser.
Peut-on trouver un algorithme qui permettent à tous les philosophes d’exercer
leurs deux activités sans être bloqués ?

Le repas des philosophes : Proposition 1

Quand un philosophe veut manger, il prend sa baguette gauche, puis sa baguette


droite. Il mange puis repose sa baguette gauche puis sa baguette droite.

Dans ce cas que se passe-t-il ? Si tous les philosophes essaient de prendre leurs
baguettes droites en même temps, il n’y aura pas assez de baguettes et tous seront
bloqués. Nous arrivons à une situation d’interblocage (voir l’algorithme 28).

Le repas des philosophes : Proposition 2

Quand un philosophe prend sa baguette gauche, il regarde si sa baguette droite


est disponible. Si elle ne l’est pas, il repose sa baguette gauche et attend un certain
temps avant de recommencer le scénario. Cette solution peut amener à une situation

65
Synchronisation entre processus (BH)

Algorithme 28 : Repas des philosophes : interblocage


Variables
nbP hilosophes : entier, initialisé à 5 // nombre de philosophes
Fonction Philosophe(int num)
begin
while TRUE do
penser()
prendreBaguette(num)
prendreBaguette((num + 1) mod nbP hilosophes)
manger()
rendreBaguette(num)
rendreBaguette((num + 1) mod nbP hilosophes)
end while
end

de famine c’est à dire à une solution où les philosophes ne seront jamais satisfaits. En
effet, s’ils cherchent à manger tous en même temps, ils reposeront leurs baguettes tous
en même temps et aucun ne pourra en profiter.
Remarque : il n’est pas raisonnable de construire un algorithme qui fonctionne
uniquement dans les cas où les philosophes agissent de façon désynchronisée, c’est à
dire cherchent à prendre leur baguette droite suivant un rythme aléatoire les uns par
rapport aux autres.

Le repas des philosophes : Proposition 3

On introduit un mutex avant la première instruction prendreBaguette et on le


rend après la dernière instruction rendreBaguette. Cette solution n’introduit pas
d’interblocage, ni de famine. Par contre, avec cette solution, un seul philosophe peut
manger à la fois, ce n’est donc pas performant. Nous cherchons une solution permet-
tant d’utiliser le maximum de baguettes.

Le repas des philosophes : Proposition 4

Dans cette solution un philosophe peut avoir trois états : FAIM, MANGE et PENSE.
L’état FAIM signifie que le philosophe informe qu’il veut passer dans l’état MANGE et
pour cela posséder deux baguettes. Un philosophe passe dans l’état MANGE, quand
il est dans l’état FAIM et que ses deux voisins ne sont pas dans l’état MANGE. Les
états des philosophes sont stockés dans un tableau partagé par tous.

66
Synchronisation entre processus (BH)

Exercice 14 : Repas des philosophes : solution 4 avec sémaphores


Écrire l’algorithme correspondant à la solution 4 du problème des philosophes en uti-
lisant des sémaphores. Faire bien attention à déterminer les sections critiques.

Exercice 15 : Repas des philosophes : solution 4 avec moniteurs


Écrire l’algorithme correspondant à la solution 4 du problème des philosophes en uti-
lisant des moniteurs.

2.7 Correction des Exercices

Correction Exercice 1 :
La condition numéro 2 n’est pas vérifiée, en effet, un processus peut être en attente
de Section Critique alors que celle-ci est libre. Un processus P1 demande la Section
Critique, la valeur de la variable Tour est égale à 2, il ne peut donc pas y entrer, et le
processus P2 n’est pas en Section Critique. P1 doit attendre que P2 entre et sorte de
Section Critique pour y entrer à son tour.

Correction Exercice 2 : Déroulement algorithme de Peterson


À l’initialisation, aucun processus ne se trouve en Section Critique.

1. P0 demande la Section Critique, puis une fois que P0 est en Section Critique, P1
demande à y entrer.
P0 demande la Section Critique, il exécute la fonction EntreeSC avec son nu-
méro égal à 0. Il positionne l’entrée correspondant à son numéro du tableau
interesse à VRAI, la variable tour à son numéro c’est à dire 0 et la variable
autre à 1. Dans la boucle while il ne réalise pas d’attente car l’autre proces-
sus n’a pas demandé la Section Critique interesse[1] = FAUX. Il entre en
Section Critique. Pendant ce temps, P1 demande la Section critique. il exécute
la fonction EntreeSC avec son numéro égal à 1. Il positionne l’entrée corres-
pondant à son numéro du tableau interesse à VRAI et la variable tour à son
numéro c’est à dire 1. Dans la boucle while il se met en attente car l’autre pro-
cessus est en Section Critique interesse[0] = VRAI. Cette attente prend fin,
quand P0 sort de Section Critique et exécute la fonction SortieSC(0). Alors
il positionne interesse[0] à FAUX ce qui permet à P1 de sortir de son attente.
2. P0 et P1 demandent la Section Critique en même temps
Les deux processus mettent à jour le tableau interesse, ainsi : interesse[0]
= VRAI et interesse[1] = VRAI

67
Synchronisation entre processus (BH)

La variable tour étant une variable partagée aura la valeur du dernier proces-
sus demandeur (en effet, les deux processus ne peuvent pas en même temps
écrire dans la même variable), par exemple, tour aura la valeur 1.
Le processus P0 n’est pas mis en attente sur le test while (une des condition
est fausse car tour = 1), alors que P1 l’est. Quand P0 sort de Section Critique,
P1 pourra y entrer.

Correction Exercice 3 : Exclusion mutuelle avec sémaphores


L’utilisation des sémaphores pour réaliser de l’exclusion mutuelle est équitable uni-
quement sur un système qui gère les files d’attente de façon équitable.

Correction Exercice 4 : Exclusion mutuelle avec sémaphores : la cuisine

Nous définissons un sémaphore semC initialisé à 1 et partagé par le code exécuté


par chacune des cuisinières.
Chacune des cuisinières Ci exécutera la fonction EntreeCuisine() lorsqu’elle
voudra accéder à la cuisine et la fonction SortieCuisine() lorsqu’elle quittera la
cuisine. Ces fonctions sont données dans l’algorithme 29.

68
Synchronisation entre processus (BH)

Algorithme 29 : Algorithme : partage d’une cuisine avec sémaphores


Variables
semC : sémaphore d’exclusion mutuelle, initialisé à 1
Fonction EntreeCuisine()
begin
// la cuisinière entre dans la cuisine si le sémaphore est libre
P(semC)
Entre dans la cuisine
end
Fonction SortieCuisine()
begin
sort de la cuisine
// la cuisinière sort de la cuisine, elle libère le sémaphore, la cuisinière (s’il y en a
une) en tête de la file d’attente attachée au sémaphore peut entrer dans la cuisine
V(semC)
end
Algorithme de chaque cuisinière Ci
begin
EntreeCuisine()
cuisiner()
SortieCuisine()
end

69
Synchronisation entre processus (BH)

C1 C2 C3 Sémaphore
semC f(semC)
1 {}
entreCuisine()
P(semC) 0 {}
cuisiner()
entreCuisine()
P(semC) -1 {C1}
C1 en attente
entreCuisine()
P(semC) -2 {C1,C3}
C3 en attente
sortieCuisine()
V(semC) -1 {C3}
Réveil
cuisiner()
sortieCuisine()
V(semC) 0 {}
Réveil
cuisiner()
sortieCuisine()
V (semC) 1 {}

TABLE 2.6 – Résultat d’exécution : partage d’une cuisine avec sémaphores

70
Synchronisation entre processus (BH)

Correction Exercice 5 : Exclusion mutuelle sur une ressource critique dispo-


nible en plusieurs exemplaires à l’aide de sémaphores

Nous définissons un sémaphore semPoele initialisé à 2 et partagé par le code


exécuté par chacun des cuisiniers.
Chacun des cuisiniers Ci exécutera la fonction PrendrePoele() lorsqu’il vou-
dra utiliser une poêle et la fonction RendrePoele() lorsqu’il rendra une poêle. Ces
fonctions sont données dans l’algorithme 30.

Algorithme 30 : Algorithme : partage de 2 poêles avec Sémaphores


Variables
semP : sémaphore d’exclusion mutuelle, initialisé à 2
Fonction PrendrePoele()
begin
// le cuisinier prend une poêle si le sémaphore est libre, sinon attend
P(semP )
Prend une poêle
end
Fonction RendrePoele()
begin
rend une poêle
// le cuisinier rend une poêle, il libère le sémaphore, le cuisinier (s’il y en a un) en
tête de la file d’attente attachée au sémaphore peut prendre la poêle libérée
V(semP )
end
Algorithme exécuté par chaque cuisinier Ci
begin
...
PrendrePoele()
utiliser poêle
RendrePoele()
...
end

71
Synchronisation entre processus (BH)

C1 C2 C3 Sémaphore
semP f(semP )
2 {}
prendrePoele()
P(semP ) 1 {}
cuisiner()
prendrePoele()
P(semP ) 0 {}
cuisiner()
prendrePoele()
P(semP ) -1 {C3}
C3 en attente
rendrePoele()
V(semP ) 0 {}
cuisiner()

rendrePoele()
V(semP ) 1 {}
rendrePoele()
V (semP ) 2 {}

TABLE 2.7 – Résultat d’exécution : partage de 2 poeles avec sémaphores

Correction Exercice 6 : Rendez-vous d’un processus avec plusieurs autres pro-


cessus avec sémaphores
Les processus Qi et le processus P partagent les variables globales suivantes :
— un sémaphore semSync initialisé à 0 : ce sémaphore permet au processus P
d’attendre que les n processus aient atteint le point de rendez-vous.
— un sémaphore semNb initialisé à 1 : permet de modifier la variable nb en exclu-
sion mutuelle.
— un sémaphore semAttend initialisé à 0 : permet aux n processus Qi d’attendre
que tout le monde ait atteint le point de rendez-vous. C’est le processus P qui
”libère” ce sémaphore, une fois le point de rendez-vous atteint.
— un entier nb initialisé à 0 : nombre de processus Qi ayant atteint le point de
rendez-vous.
L’algorithme 31 donne la fonction exécutée par chaque processus Qi
(ArrivéeQ()) ainsi que celle exécutée par le processus P (ArriveeP()) lorsqu’ils
arrivent au point de rendez-vous.
Le tableau 2.8 donne une exécution possible de l’algorithme : un processus P et
deux processus Q1 et Q2 se donnent rendez-vous.

72
Synchronisation entre processus (BH)

Algorithme 31 : Rendez-vous entre un processus Pi et N processus Qi avec sé-


maphores
Variables
semN b : sémaphore, initialisé à 1
semSynC : sémaphore, initialisé à 0
semAttend : sémaphore, initialisé à 0
nb : entier, initialisé à 0
// Fonction exécutée par P quand il atteint le rdv
Fonction ArriveeP()
begin
// attend les n processus Q
P(semSync)
Tout le monde a atteint le rdv, P réveille les N processus Q
for i=1 to N do
V(semAttend)
end for
end
// Fonction exécutée par chaque processus Qi
Fonction ArrivéeQ()
begin
// La variable nb est modifiée et consultée en exclusion mutuelle grâce au sémaphore
semNB
P(semN b)
nb ← nb + 1
// S’il s’agit du Nième processus Q, P est réveillé (ou informé)
if (nb == N ) then
V(semSync)
end if

// Libération du verrou sur nb


V(semN b)
// Attente du processus Qi s’il n’a pas atteint le point de rendez-vous
P(semAttend)
end

73
Synchronisation entre processus (BH)

P Q1 Q2 Sémaphores - Variables
semN f(semN) semS f(semS) semA f(semA) nb
1 {} 0 {} 0 {} 0
ArriveQ()
P(semN ) 0 {} 0 {} 0 {} 0
nb ← 1 0 {} 0 {} 0 {} 1
ArriveP() 0 {} -1 {P} 0 {} 1
P(semS)
P bloqué
ArriveQ()
P(semN ) -1 {Q2 } -1 {P} 0 {} 1
V(semN) 0 {} -1 {P} 0 {} 1
nb ← 2 0 {} -1 {P} 0 {} 2
P(semA) 0 {} -1 {P} -1 {Q1 } 2
Q1 Bloqué
V(semS) 0 {} 0 {} -1 {Q1 } 2
Réveil
V(semA) 0 {} 0 {} 0 {} 2
Réveil 0 {} 0 {} 1 {} 2
V(semA) V(semN ) 1 {} 0 {} 1 {} 2
P(semA) 1 {} 0 {} 0 {} 2
Continue

TABLE 2.8 – Résultat d’exécution : Rendez vous entre 1 processus P et deux processus
Q1 et Q2

Correction Exercice 7 : Exclusion mutuelle avec mutex : accès concurrent à une


cuisine
Nous définissons un mutex mutexCuisine initialisé à 1 et partagé par le code exécuté
par chacune des cuisinières.
Chacune des cuisinières Ci exécutera la fonction EntreeCuisine() lorsqu’elle
voudra accéder à la cuisine et la fonction SortieCuisine() lorsqu’elle quittera la
cuisine présentées dans l’algorithme 32.

74
Synchronisation entre processus (BH)

Algorithme 32 : Accès à une cuisine en exclusion mutuelle avec des mutex


Variables
mCuisine : mutex initialise a 1
Fonction EntreeCuisine()
begin
la cuisinière entre dans la cuisine si le sémaphore est libre prendeMutex(mCuisine)
Entre dans la cuisine
end
Fonction SortieCuisine()
begin
sort de la cuisine
// la cuisinière sort de la cuisine, elle libère le sémaphore, la cuisinière (s’il y en a une) en tête
de la file d’attente attachée au sémaphore peut entrer dans la cuisine
libererMutex(mCuisine)
end
Algorithme de chaque cuisinière Ci
begin
EntreeCuisine()
travail dans la cuisine
SortieCuisine()
end

C1 C2 C3 Mutex
mCuisine f(mCuisine)
1 {}
entreCuisine()
prendeMutex(mCuisine) 0 {}
cuisiner()
entreCuisine()
prendeMutex(mCuisine) 0 {C1 }
C1 en attente
entreCuisine()
prendeMutex(mCuisine) 0 {C1 ,C3 }
C3 en attente
sortieCuisine()
libererMutex(mCuisine) 0 {C3 }
cuisiner()
sortieCuisine()
libererMutex(mCuisine) 0 {}
cuisiner()
sortieCuisine()
libererMutex (mCuisine) 1 {}

TABLE 2.9 – Résultat d’exécution : partage d’une cuisine avec mutex

75
Synchronisation entre processus (BH)

Correction Exercice 8 : Exclusion mutuelle avec moniteurs : accès concurrent


à une cuisine

La cuisine représente la ressource critique. On définit un moniteur Cuisine pour


gérer son accès en exclusion mutuelle. La solution est donnée par l’algorithme 33

76
Synchronisation entre processus (BH)

Algorithme 33 : Accès concurrent à une cuisine à l’aide de moniteurs


Type TCuisineMoniteur Moniteur
begin
Variables
occupe : booleen
accesCuisine : condition
Fonction EntreeCuisine-Mon()
begin
// si la cuisine est déjà occupée, attente sur la condition accesCuisine
if occupe == vrai then
[Link]()
end if

// la cuisine est occupée, positionnement du booléen à vrai


occupe = vrai
end
Fonction SortieCuisine-Mon()
begin
// la cuisine est libre, positionnement du booléen à faux
occupe = faux
// réveil d’une cuisinière s’il y en a une en attente
[Link]()
end
Fonction Initialisation()
begin
occupe = faux
end
end
Variables
accesCuisineMon : TCuisineMoniteur
Fonction EntreeCuisine()
begin
[Link]-Mon()
end
Fonction SortieCuisine()
begin
[Link]-Mon()
end
// Fonction exécutée par chacune des cuisinières C − i
Fonction comportementCuisiniere()
begin
EntreeCuisine()
cuisiner()
SortieCuisine()
end

77
Synchronisation entre processus (BH)

Correction Exercice 9 : Exclusion mutuelle sur une ressource critique dispo-


nible en plusieurs exemplaires à l’aide de moniteurs

On définit un moniteur pour gérer l’accès aux 3 poêles. La solution est donnée
dans l’algorithme 34.

78
Synchronisation entre processus (BH)

Algorithme 34 : Accès à une ressource disponible en plusieurs exemplaires à


l’aide de moniteurs
Type TPoeleMoniteur Moniteur
begin
Variables
compte : entier // compte le nombre de poêles utilisées
accesPoele : condition // condition sur laquelle les cuisiniers sont mis en attente ou
réveillés
Fonction prendre()
begin
// si les 3 poêles sont utilisées, mise en attente sur condition accesPoele
If compte >= 3 [Link]()
// le cuisinier accède à une poêle, incrémentation du compteur
compte = compte +1
end
Fonction rendre()
begin
// le cuisinier libère une poêle, décrémentation du compteur
compte = compte - 1
// un cuisinier en attente d’une poêle (sur la condition accesPoele) est réveillé
[Link]()
end
Fonction Initialisation()
begin
compte = 0
end
end
Variable
poeleMon : TPoeleMoniteur
Fonction PrendrePoele()
begin
[Link]()
end
Fonction RendrePoele()
begin
[Link]()
end
// Fonction exécutée par chacune des cuisinières quand elle veut utiliser une poêle
Fonction accèsCuisinierePoele()
begin
prendrePoele()
utiliserPoele()
rendrePoele()
end

79
Synchronisation entre processus (BH)

Correction Exercice 10 : Synchronisation d’exécution : Le rendez-vous d’un


processus avec plusieurs autres processus à l’aide de moniteurs

La correction est donnée dans l’algorithme 35 et 36 . Le processus P exécute la


fonction ArriverRdvP() quand il arrive au point de rendez vous pour attendre les
autres processus. Chaque processus Qi exécute la fonction ArriverRdvQ() quand il
arrive au point de rendez-vous.
Le nième processus Q qui exécute la fonction ArriverRdvQ(), réveille un pro-
cessus Q et le processus P. Le processus Q réveillé continue son exécution (c’est à dire
après [Link]()) et réveille un processus Q et ainsi de suite ...
On suppose avec cet algorithme que le processus P arrive au point de rendez-vous
avant les processus Q.

80
Synchronisation entre processus (BH)

Algorithme 35 : Rendez-vous d’un processus avec plusieurs autres processus à


l’aide de moniteurs
Type TRendezVousMoniteur Moniteur
begin
Variables
nbArrives : entier // nombre de processus Q arrivés au point de rendez-vous
pContinue : booleen // indique si p a déjà été réveillé
tousArrive : condition // condition sur laquelle les processus Q sont mis en
attente ou réveillés
seulArrive : condition // condition sur laquelle le processus P est mis en
attente ou réveillé
Fonction ArriverQ()
begin
//incrémentation du nombre de processus Q arrivés au rdv
nbArrives=nbArrive+1
// si tous les processus Q ne sont pas arrivés au point de rdv, attente sur la
condition tousArrives
if nbArrives < N then
[Link]()
end if
// le dernier processus Q arrivé ou un précédent Q qui vient d’être réveillé,
réveille un processus Q en attente
[Link]()
// le dernier processus Q arrivé au point de rdv réveille le processus P (à l’aide de
la condition seulArrivé) : le rdv est atteint par tout le monde
if pContinue == faux then
pContinue = vrai
[Link]()
end if
end
Fonction ArriverP()
begin
// le processus p est mis en attente des processus Q sur la condition seulArrive
[Link]()
end
Fonction Initialisation()
begin
// aucun processus Q n’est encore au point de rendez-vous
nbArrives = 0
// le processus P n’a pas encore été réveillé
pContinue = faux
end
end

81
Synchronisation entre processus (BH)

Algorithme 36 : Rendez-vous d’un processus avec plusieurs autres processus à


l’aide de moniteurs - suite
Variables
rdvMon : TRendezVousMoniteur
// Fonction exécutée par le processus P quand il arrive au rendez-vous
Fonction ArriverRdvP()
begin
[Link]()
end
// Fonction exécutée chaque processus Q quand il arrive au rendez-vous
Fonction ArriverRdvQ()
begin
[Link]()
end

Correction Exercice 11 : Généralisation à plusieurs Producteurs et Consomma-


teurs à l’aide de sémaphores

La taille du buffer est n. Les consommateurs partagent la variable queue et les


producteurs la variable tête. Les accès à ces variables doivent se faire en exclusion
mutuelle, alors que cela n’était pas nécessaire dans l’exemple avec un seul producteur
et un seul consommateur. Deux sémaphores supplémentaires sont alors ajoutés pour
gérer ces accès aux variables partagées. Ces sémaphores sont des variables globales.
— semProd : sémaphore gérant l’accès à la variable tête, initialisé à 1.
— semCons : sémaphore gérant l’accès à la variable queue, initialisé à 1.
La solution est présentée dans les algorithmes 37 et 38.
Comme dans le cas avec un seul producteur et un seul consommateur, on doit
ajouter un mutex si l’indice d’accès au tampon est partagé par les producteurs et les
consommateurs.

82
Synchronisation entre processus (BH)

Algorithme 37 : Plusieurs Producteurs-Consommateurs avec sémaphores(1)


Variables
semPlein : sémaphore // sur le nombre d’emplacements occupés du tampon
semVide : sémaphore // sur nombre d’emplacements vides du tampon
semProd : sémaphore // gérant l’accès à la variable tête
semCons : sémaphore // gérant l’accès à la variable queue
tampon : tableau de n éléments
tête : entier // emplacement du tampon où le producteur écrit
queue : entier // emplacement du tampon où le consommateur lit
Fonction Initialisation
begin
Init(SemPlein, 0) // nombre d’emplacements occupé
Init(SemVide, n ) // nombre d’emplacements vides = N
Init(SemProd,)1 // l’accès à la variable tête est libre
Init(SemCons,1) // l’accès à la variable queue est libre
tête = 0 // emplacement d’écriture est 0
queue = 0 // emplacement de lecture est 0
end
Fonction Deposer(X)
begin
// pour écrire il doit rester un emplacement vide : décrémentation du sémaphore et mise en
attente sur le sémaphore semVide s’il y a lieu)
P(semVide)
// accès en exclusion mutuelle à la variable tête, à l’aide du sémaphore semProd
P(semProd)
// écriture dans le tampon
tampon[tête] = X
// mise à jour de l’emplacement d’écriture
tête = (tête + 1) mod n
// libération de l’accès à tête
V(semProd)
// libération d’un consommateur, un emplacement a été rempli : incrémentation du
sémaphore semPlein
V(semPlein)
end
Fonction Retirer(X)
begin
// pour consommer, il doit y avoir un emplacement plein au moins
P(semPlein)
// accès à la variable queue en exclusion mutuelle
P(semCons)
// consommation d’un élément
Y = tampon[queue]
// mise à jour de la variable queue
queue = (queue +1) mod n
// on libère l’accès à la variable queue
V(semCons)
// un emplacement a été libéré, incrémentation de semVide : réveil d’un producteur s’il y en
a un en attente V(semVide)
end
83
Synchronisation entre processus (BH)

Algorithme 38 : Plusieurs Producteurs-Consommateurs avec sémaphores (2)


// Fonction exécutée par chaque processus Producteur Pi
Fonction Producteur()
Variable
X : entier
begin
while vrai do
produire(X)
Deposer(X)
end while
end
//Fonction exécutée par chaque Processus Consommateur Ci
Fonction Consommateur()
Variable
X : entier
begin
while vrai do
Retirer(X)
consommer(X)
end while
end

Correction Exercice 12 : Généralisation à plusieurs Producteurs et Consomma-


teurs à l’aide de moniteur
Aucune modification ne doit être apportée. Plusieurs producteurs et consommateurs
peuvent utiliser le moniteur ProducteurConsommateur pour accéder au tampon
partagé avec le code proposé.

Correction Exercice 13 : Lecteurs-Rédacteurs : priorité aux lecteurs

Voici une exécution possible (voir la table 2.10) de la séquence proposée. Les ins-
tructions pourraient être plus imbriquées. Dans la dernière colonne sont données les
valeurs des variables et des sémaphores à l’issue de l’exécution des blocs situés à
gauche. Pour chaque sémaphore sont donnés la valeur du sémaphore e et la file d’at-
tente f.

84
Synchronisation entre processus (BH)

L1 R1 L2 Sémaphores - Variables
semE f(semE) semL f(semL) nbLect
1 {} 1 {} 0
debutLecture()
P(semL) 1 {} 0 {} 0
nbLect + + 1 {} 0 {} 1
P(semE) 0 {} 0 {} 1
V(semL) 0 {} 1 {} 1
en SC
debutEcriture()
P(semE) -1 {R1 } 1 {} 1
en attente sur semE
debutLecture()
P(semL) -1 {R1 } 0 {} 1
nbLect ++ -1 {R1 } 0 {} 2
V(semL) -1 {R1 } 1 {} 2
en SC
finLecture()
P(semL) -1 {R1 } 0 {} 2
nbLect − − -1 {R1 } 0 {} 1
V(semL) -1 {R1 } 1 {} 1
finLecture()
P(semL) -1 {R1 } 0 {} 1
nbLect − − -1 {R1 } 0 {} 0
V(semE) 0 {} 0 {} 0
en SC V(semL) 0 {} 1 {} 0

TABLE 2.10 – Lecteurs-Rédacteurs : priorité aux lecteurs

Correction Exercice 14 : Repas des philosophes : solution 4 avec sémaphores


La solution 4 au repas des philosophes avec sémaphores est donnée par l’algo-
rithme 39.
Explications :

— Les fonctions gauche(num) et droite(num) déterminent les numéros des voi-


sins gauche et droite du philosophe de numéro num.
— La fonction philosophe(num) est exécutée par chaque philosophe, elle consti-
tue son programme principal.
— On gère un tableau de sémaphores, un sémaphore par philosophe. Un philo-
sophe qui est en attente de manger est bloqué par ce sémaphore (fonction P sur
le sémaphore). L’autorisation de manger donnée à un philosophe se traduit par
un appel à la fonction V sur son sémaphore.
— La fonction prendreBaguette(num) est appelée par un philosophe de nu-
méro num qui veut manger. Dans cette fonction, on définit une section critique
pour pouvoir consulter et modifier le tableau contenant les états des proces-
sus (qui est une ressource commune). Cette section critique est accédée à l’aide
d’un mutex commun à tous les philosophes : mutexDonnees. Le philosophe
indique qu’il veut manger en passant son état à FAIM. Ensuite, il teste s’il peut
manger à l’aide de la fonction testMangeOk(num). À la sortie de la fonction
testMangeOk(num), le philosophe essaie de prendre le mutex semPhi cor-
respondant à son numéro. S’il peut manger, ce mutex a été mis à VRAI dans la

85
Synchronisation entre processus (BH)

Algorithme 39 : Repas des philosophes avec sémaphores


Variables :
Type Statut=(PENSE, MANGE, FAIM)
etat[5] : Statut // tableau contenant les états des différents processus
semPhi[5] : sémaphore, initialisés à 0 // tableau de sémaphores, un par philosophe
mutexDonnees : mutex, initialisé à 1
int Fonction gauche(int num)
begin
return((num + nbPhi −1) modulo nbPhi)
end
int Fonction droite(int num)
begin
return((num +1) modulo nbPhi)
end
Fonction Philosophe(int num)
begin
while TRUE do
penser()
prendreBaguette(num)
manger()
rendreBaguette(num)
end while
end
Fonction prendreBaguette(int num)
begin
mutexLock(mutexDonnees)
etat[num] ← FAIM
testMangeOk(num) ;
mutexUnLock(mutexDonnees)
P(semPhi[num])
end
Fonction rendreBaguette(int num)
begin
mutexLock(mutexDonnees)
etat[num] ← PENSE
testMangeOk(gauche(num))
testMangeOk(droite(num))
mutexUnLock(mutexDonnees)
end
Fonction testMangeOk(int num)
begin
if etat[num] = FAIM ET etat[gauche(num)] ̸= MANGE ET etat[droite(num)] ̸= MANGE
then
etat[num] ← MANGE
V(semPhi[num])
end if
end

86
Synchronisation entre processus (BH)

fonction testMangeOk(num), il passe donc à la fonction manger(). Sinon, il


reste bloqué sur son sémaphore et il sera libéré lorsqu’un de ses voisins appel-
lera la fonction rendreBaguette(num).
— Un philosophe qui a finit de manger exécute la fonction
rendreBaguette(num). Dans cette fonction, comme dans la précédente, une
section critique est définie. L’état du philosophe est positionné à PENSE, le
philosophe repose donc ses deux baguettes. On teste si cela permet aux voisins
de gauche et de droite de ce philosophe de manger à l’aide de la fonction
testMangeOk().
— La fonction testMangeOk(num) permet de déterminer si un philosophe peut
passer à l’état MANGE. Pour cela, il faut qu’il soit dans l’état FAIM et que ses
voisins ne soient pas dans l’état MANGE. Si c’est le cas, l’état du philosophe
est positionné à MANGE et la fonction V est appelée sur son sémaphore pour le
réveiller.

87
Synchronisation entre processus (BH)

Correction Exercice 15 : Repas des philosophes : solution 4 avec moniteurs

La solution 4 au repas des philosophes avec moniteurs est donnée par les algo-
rithmes 40 et 41 pour 5 philosophes.

88
Synchronisation entre processus (BH)

Algorithme 40 : Repas des philosophes avec moniteurs (1)


Type TRepasPhilosopheMoniteur Moniteur
begin
Variables
type Statut = (PENSE,MANGE,FAIM) // les trois statuts des philosophes
etat[5] : Status // tableau contenant le statut de chaque philosophe
AMoi[5] : condition // tableau de 5 conditions, correspondant à chacun des philosophes
Fonction PrendreBaguette(i : entier)
begin
// maj de l’état du philosophe
etat[i]=FAIM
if (etat[(i + 1) mod 5] ̸= M AN GE) ∧ (etat[(i − 1) mod 5] ̸= M AN GE) then
// si les 2 voisins de i ne sont pas en train de manger, I peut manger, maj de son état
etat[i]=MANGE
else
// sinon le philosophe est mis en attente sur sa condition
AMoi[i].wait()
end if
end
Fonction RendreBaguette(i : entier)
begin
// maj de l’état du philosophe
etat[i]=PENSE
if (etat[(i + 1) mod 5] == F AIM ) ∧ (etat[(i + 2) mod 5] ̸= M AN GE) then
// si le voisin de droite de i (i+1) est en attente de manger et que son voisin ne
mange pas alors i+1 peut manger, maj de son état et réveil
etat[(i + 1) mod 5]=MANGE
AMoi[(i + 1) mod 5].signal
end if
if (etat[(i − 1) mod 5] == F AIM ) ∧ (etat[(i − 2) mod 5] ̸= M AN GE) then
// idem avec le voisin de gauche de i (i-1)
etat[(i − 1) mod 5]=MANGE
AMoi[(i − 1) mod 5].signal
end if
end
Fonction Initialisation()
begin
initialisation des états de tous les philosophes à PENSE
for i=0 to 4 do
etat[i]=PENSE
end for
end
end

89
Synchronisation entre processus (BH)

Algorithme 41 : Repas des philosophes avec moniteurs (2)


Variable
repasPhiloMon : TRepasPhilosopheMoniteur
// Fonction exécutée par chaque philosophe Pi
Fonction Philosophe(i : int)
begin
// Comportement de chaque philosophe
while vrai do
penser()
[Link](i)
manger()
[Link](i)
end while
end

90
Chapitre 3

Synchronisation en Java (LP)

Contenu
3.1 Synchronisation des threads Java . . . . . . . . . . . . . . . . . . . . 92
3.1.1 Gestion de l’exclusion mutuelle . . . . . . . . . . . . . . . . . . 92
3.1.2 Atomicité ou les limites de l’exclusion mutuelle . . . . . . . . 97
3.1.3 Synchronisation sur les objets, le modèle de moniteur Java . . 99
3.1.4 Risque d’interblocage . . . . . . . . . . . . . . . . . . . . . . . 101
3.1.5 Le package [Link] . . . . . . . . . . . . . 102
3.2 Problèmes classiques de synchronisation . . . . . . . . . . . . . . . . 102
3.2.1 Le problème des producteurs / consommateurs . . . . . . . . 102
3.2.2 Le problème des lecteurs/rédacteurs . . . . . . . . . . . . . . 103
3.3 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
3.4 Solutions des exercices . . . . . . . . . . . . . . . . . . . . . . . . . . 105

Nous avons vu précédemment que dans un système centralisé l’exécution de plu-


sieurs processus (ou threads) qui partagent de la mémoire ou des ressources pose des
problèmes de synchronisation. Ce chapitre est une illustration des concepts présentés
dans le chapitre 2. Les problèmes classiques de synchronisation y sont évoquées sous
l’angle de la programmation multi-thread Java. Ce langage offre en effet des fonction-
nalités pour que les applications multi-threads donnent des résultats conformes aux
attentes des programmeurs et que les erreurs classiques rencontrées dans ce type de
programmation puissent être évitées.
Nous ne donnons cependant pas une description exhaustive des possibilités de
synchronisation en Java et des packages qui y sont liés. En particulier, nous n’abor-
dons pas les packages avancés de Java comme [Link] qui permet
l’utilisation directe de sémaphore ou de barrière de synchronisation.
À l’issue de ce chapitre, vous serez en mesure de :
— utiliser les structures natives du langage Java synchroniser des threads ;
— identifier l’ensemble des verrous qu’un programme utilise ainsi que chaque
verrou utilisé par un thread donné ;
— utiliser le mécanisme de monitor proposé par Java ;
— programmer des algorithmes d’exclusion mutuelle en Java ;
— déterminer les risques d’interblocages posés par un programme Java.

91
Synchronisation en Java (LP)

3.1 Synchronisation des threads Java

Dans cette première partie nous mettons en évidence, dans le contexte de pro-
grammes Java, les problèmes de synchronisation qui peuvent être rencontrés et leur
solutions. Il est donc une illustration du chapitre 2, où la notion de processus est re-
présentée par les threads.

3.1.1 Gestion de l’exclusion mutuelle

Ce paragraphe traite du problème de l’exclusion mutuelle, c’est-à-dire de la ges-


tion de l’accès concurrent à une ressource. En programmation multi-threads, la notion
de ressource critique est très présente puisque tous les threads d’une même applica-
tion partagent le même espace d’adressage, donc les mêmes variables. Comme il a été
montré dans l’exemple du chapitre 1, des threads peuvent modifier une donnée par-
tagée. Il est donc intéressant de voir ce que le langage Java prévoit pour sécuriser le
développement d’une application multi-threads et rendre à coup sûr son comporte-
ment déterministe.

Mise en évidence des données liées

Un exemple classique sujet à des problèmes de synchronisation est la modification


d’une structure (un objet par exemple) de manière concurrente par plusieurs thread.
Supposons que la donnée à mettre à jour est formée par deux champs : un nom et
une adresse. En multi-threads, si la donnée est partagée, le scénario optimiste est le
changement du nom, puis de l’adresse par un thread sans interférence avec les autres
threads. Le scénario problématique est le suivant : un thread modifie la donnée en
commençant par la modification du nom, un autre thread prend la main et modifie
l’ensemble de la donnée (nom et adresse) puis le premier thread reprend la main et
modifie l’adresse. La structure de donnée est alors dans un état incohérent avec le
nom issu de la modification du second thread alors que l’adresse est issue de la modi-
fication du premier thread. Le listing 3.1 est une illustration de ce cas de figure.

Listing 3.1 – Exemple de données liées


1 class MesCoordonnees {
2 String nom ;
3 String adresse ;
4 }
5
6 class ModifDonnees extends Thread {
7
8 static MesCoordonnees c ;
9 static int i = 0 ;
10
11 String monNom ;
12 String monAdresse ;

92
Synchronisation en Java (LP)

13
14 private int moi;
15
16 ModifDonnees (String n, String a, int moi) {
17 monNom = n ;
18 monAdresse = a ;
19 [Link] = moi;
20 }
21
22 public void run() {
23
24 [Link] = monNom ;
25
26 // simulation d’un temps de traitement long entre les deux
27 // initialisations + manipulation pour que le thread 2
28 // finisse son exection avant le thread 1
29 if (i < 1)
30 try {
31 i ++ ;
32 [Link](1000);
33 }
34 catch (InterruptedException e){ ... };
35
36 [Link] = new String (monAdresse) ;
37
38 [Link]( "Les Donnees de " + moi + " partagees : "
39 + [Link] + " " + [Link] );
40 }
41
42 public static void main(String args[]) {
43 Thread t1 = new ModifDonnees ( "Martin" , "rue du Nord" , 1);
44 Thread t2 = new ModifDonnees ( "Durand" , "rue du Sud" , 2);
45 c = new MesCoordonnees () ;
46 [Link]();
47 [Link]();
48 try {
49 [Link]();
50 [Link]();
51 }
52 catch (InterruptedException e){ ... }
53
54 [Link]( "Mes Donnees courantes : " + [Link]
55 + " " + [Link] );
56 }
57 }

Le résultat de l’exécution de ce programme Java est le suivant :

Les Donnees de 2 partagees : Durand rue du Sud


Les Donnees de 1 partagees : Durand rue du Nord
Mes Donnees courantes : Durand rue du Nord

Dans cet exemple, l’objet c est partagé entre tous les threads des objets

93
Synchronisation en Java (LP)

ModifDonnees puisqu’il est déclaré comme static. la mise à jour des données de
l’objet c est alors incorrecte car, les deux threads mis en concurrence, de manière un
peu forcée pour les besoins de l’exemple, conduisent à une mise à jour incorrecte de
l’objet c. En effet, à l’issue de l’exécution du code, il a le nom attribué par le thread t2
et l’adresse attribuée par le thread t1.
À noter que ce schéma problématique s’applique aussi au cas de tous les objets,
static ou non, qui sont partagés par des threads. Ainsi, nous aurions le même pro-
blème si nous avions créé l’objet c dans le main et que nous l’avions passé en para-
mètre de la création des threads 1
De manière plus large le problème posé est lié à deux facteurs. Le premier facteur
est que nous ne pouvons pas savoir quelle donnée est modifiée par quel thread et
à quel moment. La décision de l’ordre d’exécution est en effet prise au moment de
l’exécution par l’ordonnanceur de la JVM. Nous devons donc anticiper les différents
cas d’exécution pour éviter tout problème. Le second problème est que les threads
partagent des données et que la concurrence engendre des risques sur leur cohérence.
Les problèmes de synchronisation sont rapidement très complexes dès qu’ils font
intervenir plus de quelques threads. Le mieux est donc de les éviter au maximum.
Pour cela il faut éviter autant que possible le partage des données. Cela peut se faire :
— en programmant de manière à ce que chaque thread ait des données qui lui
sont propres, non partagées avec les autres threads,
— en utilisant prioritairement les variables locales non static des fonctions ne sont
pas partagées et ne posent donc pas de problème.
Il y a malheureusement de nombreux cas où on nous ne pouvons éviter de partager
un objet. Pour ces cas le paragraphe suivant donne les solutions permettant de garantir
l’exclusion mutuelle.

Solutions

Dans l’exemple vu la solution est de protéger la modification des données, c’est-


à-dire d’empêcher l’accès de tout autre thread, pendant le temps de l’opération de
modification-affichage des données. À la fin de la méthode, l’accès à la donnée peut
être relâché.
Le chapitre 2 propose l’utilisation d’un mutex pour gérer ce problème d’exclusion
mutuelle. Le langage Java offre un mécanisme équivalent aux applications. À tout objet
Java est associé un verrou, appelé monitor. Il sert de base à la définition des sections
critiques, marquées à l’aide du mot clé synchronized. Ce mot clef est associé à une
méthode ou à un bloc de code. La prise ou le relâchement du verrou se fait alors
1. Nous rappelons qu’en Java les variables objets sont en fait des références sur les objets corres-
pondant. L’affectation d’une variable objet à une autre variable d’objet n’entraîne donc que la recopie
de la référence dans la variable cible et donc l’accès au même objet par les deux variables. Ce partage
peut donc également conduire à un problème de concurrence si les deux variables sont utilisées par des
threads différents.

94
Synchronisation en Java (LP)

automatiquement lors de l’accès à ce bloc de code ou à la méthode. L’utilisation du


mot clé synchronized peut se faire à 3 niveaux :

1. Restriction de l’accès à une section critique en prenant le verrou d’un objet


grâce au mot clé synchronized pendant l’exécution d’un bloc de code. Ainsi,
si un thread exécute déjà cette portion de code, un second thread ne pourra pas
exécuter de section critique protégée par le même verrou en parallèle. Ce thread
sera alors placé en attente de la libération du verrou par le premier thread. Le
schéma de code est le suivant :
synchronized (monObjet) {
... section critique ...
}

Comme il existe un verrou par instance, cette section critique peut être exécutée
en parallèle si l’objet référencé par monObjet est distinct dans chaque thread.
2. Restriction de l’accès à une ou plusieurs méthodes d’une instance d’objet
en les définissant synchronized. Si un thread invoque l’une de ces mé-
thodes, alors plus aucun autre thread ne pourra invoquer les autres méthodes
synchronized de la même instance d’objet tant que le verrou n’est pas relâ-
ché :
synchronized TypeRetour1 maMethode1 ( ... ) { ... }
synchronized TypeRetour2 maMethode2 ( ... ) { ... }

Il s’agit d’un mécanisme gérant l’exclusion mutuelle entre les méthodes d’une
même instance car le verrou manipulé est propre à cette instance.
3. Restriction de l’accès à une ou plusieurs méthodes de classe. Cela fait référence
à un autre type de verrou, non plus un verrou sur l’objet, mais un verrou sur la
classe et s’applique aux méthodes static de cette classe. Ainsi, aucun autre
thread que celui entré en section critique ne peut accéder aux méthodes sta-
tiques synchronized de la classe de l’objet appelant :
class MesObjets {
static synchronized TypeRetour1 maMethode1 ( ... ) { ... }
static synchronized TypeRetour2 maMethode2 ( ... ) { ... }
}

Lorsque l’accès à un objet ou à une méthode synchronized se termine, le verrou


est libéré et peut être repris par le prochain thread mis en attente dans sa demande
d’accès à la section critique occupée. Cependant, des méthodes existent pour rendre
le verrou avant la fin de l’exécution de la méthode. Dans ce cas un autre thread peut
accéder à la ressource critique. La fin de l’exécution de la section critique par le premier
thread se fera dans un deuxième temps et lorsque cela sera possible (i.e., à la libération
du verrou).
En reprenant l’exemple précédent, il est facile de voir la portion de code sur la-
quelle les modifications de la structure de type MesCoordonnees sont faites. Ces

95
Synchronisation en Java (LP)

modifications portent justement sur la même instance d’un objet de ce type, ce qui
permet l’utilisation de la synchronisation d’une méthode ou d’une partie de code. La
solution proposée consiste à poser un verrou sur cet objet. Ainsi, il suffit d’isoler le
code représentant la section critique sur l’objet c avec le mot clé synchronized en
s’assurant que l’objet référencé par c sera le même pour tous les threads (sinon, le ver-
rou ne sera pas unique). C’est le cas car c est statique. Le listinbg 3.2 montre comment
l’exemple précédent peut être corrigé pour respecter l’objectif initial de l’application.

Listing 3.2 – Exemple correct de données liées


1 class MesCoordonnees {
2 String nom ;
3 String adresse ;
4 }
5
6 class ModifDonnees extends Thread {
7
8 static MesCoordonnees c ;
9 static int i = 0 ;
10
11 String monNom ;
12 String monAdresse ;
13
14 private int moi;
15
16 ModifDonnees (String n, String a, int moi) {
17 monNom = n ;
18 monAdresse = a ;
19 [Link] = moi;
20 }
21
22 public void run() {
23
24 synchronized ( c ) {
25 [Link] = monNom ;
26
27 // simulation d’un temps de traitement long entre les deux
28 // intialisations + manipulation pour que le thread 2
29 // finisse sont exection avant thread 1
30 if (i < 1)
31 try {
32 i ++ ;
33 [Link](1000);
34 }
35 catch (InterruptedException e){ ... };
36
37 [Link] = new String (monAdresse) ;
38
39 [Link]( "Les Donnees de " + moi + " partagees : "
40 + [Link] + " " + [Link] );
41 }
42 }
43

96
Synchronisation en Java (LP)

44 public static void main(String args[]) {


45 Thread t1 = new ModifDonnees ( "Martin" , "rue du Nord" , 1);
46 Thread t2 = new ModifDonnees ( "Durand" , "rue du Sud" , 2);
47 c = new MesCoordonnees () ;
48 [Link]();
49 [Link]();
50 try {
51 [Link]();
52 [Link]();
53 }
54 catch (InterruptedException e){ ... }
55
56 [Link]( "Mes Donnees courantes : "
57 + [Link] + " " + [Link] );
58 }
59 }

Le résultat de l’exécution de ce programme Java est maintenant systématiquement


correct. Le résultat est le suivant :

Les Donnees de 1 partagees : Martin rue du Nord


Les Donnees de 2 partagees : Durand rue du Sud
Mes Donnees courantes : Durand rue du Sud

3.1.2 Atomicité ou les limites de l’exclusion mutuelle

Dans le cas précédent, nous avons plusieurs données qui sont modifiées par plu-
sieurs de threads. La mise en évidence de la concurrence et de la nécessité de l’exclu-
sion mutuelle est facile. Dans certain cas, cette nécessité apparaît de manière moins
évidente. Nous nous proposons ici de voir les limites de l’atomicité dans le langage
Java, donc les limites à partir desquelles l’exclusion mutuelle est nécessaire.
Nous nous proposons d’étudier le programme donné par le listing 3.3, proche de
celui vu au chapitre 1.

Listing 3.3 – compteur multi-threadé


1 class Counter {
2
3 int value;
4 public Counter() { value = 0; }
5 public void increm() { value++; }
6 }
7
8 class MonThread extends Thread {
9
10 Counter count;
11 int nbIter = 1000;
12
13 public MonThread( Counter ref ) { count = ref; }

97
Synchronisation en Java (LP)

14 public void run() {


15 for ( int i = 0 ; i < nbIter ; i++ ) [Link]();
16 }
17 }
18
19 public class counterMultiCore {
20
21 static Counter mc;
22
23 public static void main(String args[]) {
24
25 try {
26 // Data to be shared
27 mc = new Counter();
28
29 MonThread mt1 = new MonThread( mc );
30 MonThread mt2 = new MonThread( mc );
31
32 [Link]();
33 [Link]();
34 [Link]();
35 [Link]();
36
37 [Link]( " a= " + [Link]);
38
39 } catch ( Exception ex ) {
40 [Link]( ex );
41 }
42 }
43 }

Le code de la classe Counter est typiquement ce que nous pourrions écrire pour
compter le nombre d’accès à un objet, une donnée, une page, . . .. Il ne semble pas y
avoir de problèmes d’exclusion puisque nous ne réalisons qu’une seule instruction
(incrémentation) sur une seule donnée (count).
Dans l’exemple donné la classe Counter est utilisée par deux threads. Si vous
compiler ce code et que vous l’exécutez sur un processeur multi-cœeur, le résultat
obtenu varie : 1643, 1867, . . . et parfois 2000. Ce résultat est faux, parce qu’il n’est
pas toujours égal à 2000, chaque thread incrémentant 1000 fois le compteur le résultat
devrait être 2000, et surprenant parce qu’il varie.
Il s’agit ici d’un problème d’atomicité. En effet l’instruction value++ n’est pas ato-
mique mais plutôt composée de 3 instructions : chargement depuis la mémoire de la
valeur de value dans un registre, incrémentation de la valeur puis sauvegarde en mé-
moire. Si deux threads exécutent cette instruction de manière parfaitement parallèle,
que va-t-il se passer ? Ils vont, tous les deux charger la valeur x de la variable dans
les registres de leur propre cœur, incrémenter en même temps la valeur du registre,
ce qui fait que le registre contient ensuite la valeur x+1, et enregistrer en mémoire
cette valeur. A l’issue de l’opération la variable value contient x+1 alors que deux

98
Synchronisation en Java (LP)

threads l’ont incrémentée, sa valeur devrait donc être x+2. En fait, tant que le premier
thread n’a pas enregistré sa valeur en mémoire il ne faut pas que le second thread lise
la valeur, nous avons bien ici une section critique qui peut donc être résolue par l’utili-
sation du qualificatif synchronized sur la fonction d’incrémentation ou simplement
autour le l’instruction d’incrémentation 2 . Le verrou de l’objet count est bien partagé
entre les deux threads et peut donc servir pour l’exclusion mutuelle.
Ce problème pose donc la question de savoir jusqu’où aller dans la mise en œuvre
de l’exclusion mutuelle. La solution la plus sûre serait de mettre tout le code multi-
threadé en exclusion mutuelle. Nous aurions ainsi la garantie qu’il n’y ait pas de pro-
blème. Cette solution n’est cependant pas souhaitable car le coût engendré est non
négligeable : cela réduit le parallélisme et donc les performances et la souplesse ap-
portées par le multi-threading. Dans le pire des cas nous arriverions à rendre le code
complètement séquentiel, ce qui rendrait inutile l’utilisation des threads.
Se pose donc le problème de savoir ce qui doit être protégé ou non. Nous avons
vu au chapitre 2 la notion d’instruction atomique qui est forcément exécutée en une
seule fois et n’a donc pas besoin d’être protégée. Elles constituent donc la limite de
la protection et nous voyons donc qu’il est nécessaire de connaître la limite de l’ato-
micité des instructions de Java. Les seules instructions atomiques sont les lectures et
écritures des types primitif (excepté long et double), des références d’objets, et de
toutes les variables déclarées volatile. La recopie d’une référence d’objet (la nou-
velle référence pointe sur le même objet) n’a donc pas besoin d’être protégée mais la
recopie d’un objet (création d’une autre instance) doit l’être.

3.1.3 Synchronisation sur les objets, le modèle de moniteur Java

Comme il a été vu précédemment, à l’intérieur d’une section critique protégée par


le mot clé synchronized, un verrou est pris sur un objet. Aucun autre thread ne
peut donc accéder à cette section critique avec le même verrou avant que ce dernier
ne soit libéré. Pour faciliter cette gestion chaque objet Java peut-être vu comme un
moniteur avec une seule file d’attente, celle qui est liée à son verrou. Les méthodes
permettant de manipuler le monitor sont wait(), notify() ou notifyAll(). La
notion de moniteur en Java est assez proche notion de monitor vue au chapitre 2. Le
comportement de ces trois méthodes est le suivant :
— [Link]() libère le verrou de l’objet unObjet et bloque le thread jus-
qu’à ce qu’un appel aux méthodes notify() ou notifyAll() de l’objet
unObjet ne le réveille. Une interruption ou un time out peut aussi conduire
au déblocage du thread. Une fois débloqué, il est mis en attente pour l’accès ex-
clusif à la section critique. Il passe donc en mode prêt et il attendra de reprendre
le verrou pour continuer son exécution.
2. Attention alors à bien prendre le verrou le l’objet Counter et pas celui de l’objet MonThread
puisque, chaque thread étant lié à un objet distinct, celui ne pourra pas servir de garde à l’exclusion
mutuelle

99
Synchronisation en Java (LP)

— [Link]() réveille un thread bloqué par un appel à la méthode


wait() de l’instance unObjet. Si aucun thread n’est bloqué sur ce verrou,
rien ne se passe.
— [Link]() réveille tous les threads bloqués par un wait()
(uniquement sur l’instance unObjet). C’est le thread qui prend le premier le
verrou qui accède à la section critique (les autres y accédant par la suite).
Attention l’appel à ces trois fonctions suppose d’avoir auparavant pris le verrou
correspondant, donc d’être dans le bloc synchronized correspondant. Sinon, si ces
fonctions sont appelées sur un autre verrou ou si l’application ne possède pas le ver-
rou, elle reçoit une exception IllegalMonitorStateException.
Il faut également être prudent sur l’utilisation des moniteurs lorsque les threads
sont en plus synchronisés avec un join. En effet, le thread utilisant cette fonction se
met en attente d’un signal notify qui le réveillera, sur le moniteur de l’objet thread
qui est attendu. Il est donc préférable dans ce cas d’utiliser un objet distinct si une syn-
crhonisation doit être mise en place sur ce même thread mais pour une autre raison.
L’exemple du listing ??uivant montre une implémentation simple utilisant les mo-
niteurs Java. Le premier thread se met en attente et est réveillé par le second thread
après un temps d’attente. A noter qu’ici nous utilisons un objet spécifique comme mo-
niteur pour ne pas interférer avec le join.

Listing 3.4 – Exemple d’utilisation d’un moniteur Java


1 class MyThread1 extends Thread {
2
3 Object synchro;
4
5 public MyThread1(Object o) { synchro = o; }
6 public void run () {
7 try {
8 synchronized(synchro) {
9 [Link]("Thread " + [Link]() + " bf wait");
10 [Link]() ;
11 [Link]("Thread " + [Link]() + " af wait");
12 }
13 } catch (InterruptedException ie){ [Link]("Interrupted");}
14 }
15 }
16
17 class MyThread2 extends Thread {
18
19 Object synchro;
20
21 public MyThread2(Object o) { synchro = o; }
22 public void run () {
23 try {
24 [Link](2000);
25 [Link]("Thread " + [Link]() + " notify");
26 synchronized(synchro) { [Link](); }
27 } catch (InterruptedException ie){ [Link]("Interripted");}

100
Synchronisation en Java (LP)

28 }
29 }
30
31 public class SimpleWaitNotify {
32 public static void main ( String args[] ) {
33
34 Object obj = new Object();
35
36 MyThread1 mt1 = new MyThread1 ( obj ) ;
37 MyThread2 mt2 = new MyThread2 ( obj ) ;
38 [Link] () ;
39 [Link] () ;
40 try {
41 [Link]();
42 [Link]();
43 } catch (InterruptedException ie){ [Link]("Interripted");}
44 }
45 }

Les exercices sur la synchronisation donnés en fin de chapitre permettent de se


familiariser avec la programmation des moniteurs en Java.

3.1.4 Risque d’interblocage

Dans le cas d’une programmation utilisant la restriction d’accès à certains objets,


il est possible de prendre plusieurs verrous en cascade, par exemple dans le cas de
plusieurs appels de méthodes imbriqués : synchronized (o1) { synchronized
(o2) { ...} ... }. Un risque d’interblocage (voir le chapitre 2) existe si deux
threads demandent les verrous aux objets o1 et o2 de manière concurrente et croi-
sée. Pour éviter une telle situation, une hiérarchie peut être établie entre les objets à
synchroniser et l’ordre des appels. Si la prise des verrous se fait dans l’ordre de cette
hiérarchie, l’interblocage n’aura pas lieu (le graphe de dépendance est alors réduit à
un arbre).
Dans ce type de configuration où l’on synchronise des objets en cascade, il existe
un risque supplémentaire dès lors que l’on souhaite bloquer un thread par une ins-
truction wait(). Soit l’exemple suivant : synchronized (o1) { synchronized
(o2) { [Link] (); ...} ... }. Dans ce cas, la section critique est libérée sur
o1 alors qu’elle ne l’est pas sur o2.
Une autre source de difficulté tient au fait que rien n’assure qu’un thread débloqué
puisse accéder à la section critique. Avant cela, il doit prendre le verrou. Comme en
plus, l’ordonnancement n’offre aucune garantie, les conditions de blocage du thread
peuvent être à nouveau réunies et doivent être testées avant l’entrée en section critique
(voir l’implémentation des sémaphores).
En synchronisation il est rapide de s’apercevoir que les problèmes deviennent ra-
pidement complexes et génèrent des bugs difficiles à corriger. Par exemple, certains in-

101
Synchronisation en Java (LP)

terblocages ne se produisent que lorsqu’une configuration particulière arrive puisque


cela dépend de la manière dont sont ordonnancés les threads, qui, à sont tour, dépend
de la manière dont l’ordonnanceur les traite, qui, à son tour dépend des évènements
qui se produisent sur l’ordinateur. Un interblocage peut ainsi n’arriver que très rare-
ment... et il peut donc être très difficile d’en trouver la raison. Il est donc fortement
recommandé de prévoir des schémas de synchronisation les plus simples possibles
pour être capable d’en comprendre le déroulement au moment de la mise au point.

3.1.5 Le package [Link]

Nous n’abordons dans ce cours que les principes classiques et basiques de la ges-
tion de la synchronisation dans Java, pour illustrer les problèmes posés au chapitre 2. Il
existe cependant des outils très élaborés pour résoudre les problèmes de concurrence
dans le package [Link]. Par exemple ce package propose une im-
plémentation native et efficace des mutex (Lock) et des sémaphores. Il offre également
des structures de données, telles que des map, garanties contre les problèmes d’accès
concurrents. Un des autres intérêts de ce package, et des outils qu’il propose, est l’ef-
ficacité de la mise en œuvre. En effet, la résolution de conflits de concurrence passe
souvent par une sérialisation des traitements, ce qui ralentit l’exécution, et la mise en
œuvre de structures adaptées suppose souvent une compétence assez pointue dans ce
domaine. Malgré tout l’utilisation de ce package reste plutôt conseillée aux program-
meurs ayant une bonne connaissance des problèmes de concurrence et de la manière
de les résoudre.

3.2 Problèmes classiques de synchronisation

3.2.1 Le problème des producteurs / consommateurs

Le problème des producteurs/consommateurs, présenté au chapitre 2, peut être


implémenté facilement en Java. En effet, chaque acteur de ce problème est un thread
qui s’exécute concurremment avec les autres. La difficulté est de prévenir les accès
en lecture sur un buffer vide et les écritures sur un buffer plein. Pour cela, deux sé-
maphores sont utilisés comme expliqué lors de la présentation du problème. Il est
rappelé ici que le buffer de lecture/écriture n’est pas la seule donnée partagée. Il faut
veiller également à ce que l’accès aux variables stockant respectivement la position de
la lecture et la position de l’écriture dans le buffer puissent être accédées de manière
exclusive. Dans le paragraphe consacré aux exercices, il est demandé d’écrire un pro-
gramme Java permettant de gérer une situation du type producteurs/consommateurs.
Une solution à cet exercice est donnée dans la section 3.4.

102
Synchronisation en Java (LP)

3.2.2 Le problème des lecteurs/rédacteurs

Le problème des lecteurs/rédacteurs a été défini dans le chapitre 2 et une solution


a été proposée. Un exercice est donné en fin de chapitre pour une implémentation en
Java de cette solution.

3.3 Exercices

Exercice 1 : Exclusion mutuelle avec la synchronisation

Soit l’exemple donné dans le paragraphe 3.1.1. Il s’agit d’un exemple de code Java
dans lequel deux threads cherchent à mettre à jour une variable partagée.
➽ Écrire le programme permettant de faire la même chose que le programme donné
en exemple, à la différence que cette mise à jour se fera par l’intermédiaire d’une mé-
thode dans la classe de l’objet MesCoordonnees. Le code demandé ici correspond à
la solution avec la gestion correcte de l’exclusion mutuelle.

Exercice 2 : Écriture des sémaphores en Java

➽ Écrire une implémentation possible des sémaphores en Java. Attention, lorsque la


valeur du sémaphore devient nulle, toute nouvelle demande du sémaphore conduit
au blocage du thread appelant. De plus, lors du déblocage d’un thread lorsque le sé-
maphore est rendu et reprend une valeur supérieur à zéro, il faut tester à nouveau la
valeur du sémaphore avant de rentrer en section critique. En effet, il se peut que des
actions soient réalisées entre le déblocage du thread (thread prêt) et la prise du verrou.
Un autre thread peut très bien prendre le sémaphore avant le sémaphore dernièrement
débloqué, provoquant ainsi à nouveau son blocage.

Exercice 3 : Écriture d’un programme Java du problème des producteurs / consomma-


teurs

➽ Écrire une implémentation possible du problème des producteurs/consommateurs


posé dans le chapitre 2, avec un producteur et un consommateur manipulant un buf-
fer d’objets (Integer). L’utilisation des sémaphores n’est pas indispensable si on gère
l’espace occupé dans le buffer par les méthodes put() et get() permettant respecti-
vement de produire une donnée et de la consommer. De même, la gestion des données

103
Synchronisation en Java (LP)

partagées accessibles uniquement grâce à la prise d’un mutex peut être gérée en utili-
sant les facilités offertes Java.

Exercice 4 : Écriture d’un programme Java du problème des lecteurs / rédacteurs


(priorité à la lecture)

Question 4.1 :
➽ Écrire une implémentation possible du problème des lecteurs/rédacteurs comme
il a été défini dans le chapitre 2 et dans le paragraphe 3.2.2. Comme pour l’exercice
précédent, l’utilisation des sémaphores peut être complètement intégrée dans le code
grâce à la maintenance d’un compteur du nombre de lecteurs en cours de lecture d’une
part, et en fixant un mode d’accès à l’objet partagé (lecture ou rédaction) d’autre part.
Les mutex associés peuvent alors être gérés par des variables booléennes partagées
dont l’accès est protégé par le mot clé synchronized. Suivant leur valeur, l’accès à
la lecture ou à la rédaction sera possible ou non.
Le programme principal comprend 5 lecteurs et 5 rédacteurs qui demandent res-
pectivement 5 lectures et 2 rédactions chacun. Le programme doit être conçu pour que
des affichages permettent de suivre l’évolution de l’accès à la ressource (par exemple
le contenu modifiable d’un journal). Il est possible par exemple de noter le nom du
thread, ce qu’il fait (lecture, fin de lecture, rédaction, fin de rédaction) et combien reste-
t-il de lecteurs en cours de lecture dans le cas où cela est possible.

Question 4.2 :
➽ Expliquer pourquoi, lorsqu’il y a une rédaction après un certain nombre de lectures,
il y a beaucoup d’autres rédactions et pourquoi lorsqu’il y a une lecture, d’autres lec-
tures l’accompagnent.

104
Synchronisation en Java (LP)

3.4 Solutions des exercices

Correction Exercice 1 : Exclusion mutuelle avec la synchronisation

➽ Le programme demandé pour cet exercice est le suivant :

Listing 3.5 – Exclusion mutuelle avec la synchronisation


1 class MesCoordonnees {
2 String nom ;
3 String adresse ;
4
5 public synchronized void maj ( String n, String a, int numThread ) {
6 nom = n ;
7 // simulation d’un temps de traitement long entre les deux
8 // intialisations + manipulation pour que le thread 2
9 // finisse sont exection avant thread 1
10 if (numThread < 2)
11 try {
12 [Link]().sleep(1000);
13 }
14 catch (InterruptedException e){ ... };
15
16 adresse = a ;
17 }
18
19 public synchronized void print(int numThread){
20
21 [Link]( "Les Donnees de " + numThread + " partagees : "
22 + [Link] + " " + [Link] );
23 }
24 }
25
26 class ModifDonnees extends Thread {
27
28 static MesCoordonnees c ;
29 static int i = 0 ;
30
31 String monNom ;
32 String monAdresse ;
33
34 private int moi;
35
36 ModifDonnees (String n, String a, int moi) {
37 monNom = n ;
38 monAdresse = a ;
39 [Link] = moi;
40 }
41
42 public void run() {

105
Synchronisation en Java (LP)

43
44 [Link]( monNom, monAdresse, moi ) ;
45 [Link]( moi );
46 }
47
48 public static void main(String args[]) {
49 Thread t1 = new ModifDonnees ( "Martin" , "rue du Nord" , 1);
50 Thread t2 = new ModifDonnees ( "Durand" , "rue du Sud" , 2);
51 c = new MesCoordonnees () ;
52 [Link]();
53 [Link]();
54 try {
55 [Link]();
56 [Link]();
57 }
58 catch (InterruptedException e){ ... }
59
60 [Link]( "Mes Donnees courantes : "
61 + [Link] + " " + [Link] );
62 }
63 }

➽ Le simple fait de supprimer le mot clé synchronized conduit à la trace suivante :

Les Donnees de 2 partagees : Durand rue du Sud


Les Donnees de 1 partagees : Durand rue du Nord
Mes Donnees courantes : Durand rue du Nord

Correction Exercice 2 : Écriture des sémaphores en Java

La mise en garde faite dans l’énoncé se traduit par la boucle while dans la mé-
thode P(). En effet, lorsqu’un thread rend le sémaphore par la méthode V(), il dé-
bloque un thread bloqué. La reprise de son activité a lieu, lorsqu’il a à nouveau la
main sur le processeur (et rien ne permet de savoir quand), dans la méthode P(), là
où il a été bloqué. Or entre le réveil et la prise effective du sémaphore, le sémaphore
ne sera peut être plus libre (repris par un autre thread également en attente), d’où la
nécessité de tester à nouveau sa valeur. La meilleure façon de le faire est d’itérer sur
ce test, d’où l’instruction while.

Listing 3.6 – Implémentation des sémaphores en Java


1 public class Semaphore {
2 private int cpteur = 0;
3
4 // constructeur
5 public Semaphore ( int c ) { cpteur = c; }
6
7 // prendre le semaphore

106
Synchronisation en Java (LP)

8 public synchronized void P () throws InterruptedException {


9 while ( cpteur == 0 ) { [Link] () ; }
10 cpteur -- ;
11 }
12
13 // rendre le semaphore
14 public synchronized void V () {
15 cpteur ++ ;
16 [Link] () ;
17 }
18 }

Correction Exercice 3 : Écriture d’un programme Java du problème des produc-


teurs / consommateurs

La solution proposée ici n’utilise pas les sémaphores pour gérer la taille du buffer.
Cette gestion est complètement intégrée à la classe Buffer qui offre des mécanismes
identiques pour les méthodes put() et get() grâce aux fonctions offertes par Java
mais, en utilisant l’implémentation de l’exercice précédent la réalisation serait simple
puisque proche de ce qui a été fait au chapitre 2. Le mutex dont il est question dans
l’algorithme développé dans le chapitre 2 est ici implicitement géré par le mot clé
synchronized qui interdit un accès concurrent au buffer et aux variables des indices
de lecture ou d’écriture. Lorsqu’il y a concurrence, le thread demandant l’accès à une
méthode synchronized déjà en exécution s’endort. Ce thread redevient prêt lorsque
le verrou sur la méthode est rendu. Cela ne signifie pas qu’il aura aussitôt la main sur
la méthode demandée, un autre thread peut lui ravir encore une fois l’accès à cette
méthode. Notons que toutes les méthodes synchronized sur l’objet appelant sont
inaccessibles pour tous les threads concurrents. Ceci garantit bien un fonctionnement
identique à celui proposé avec le mutex de la correction du chapitre 2. Il en est de
même avec la gestion de l’espace occupé par les informations dans le buffer et des
valeurs d’indices de lecture ou d’écriture. Comme dans le cas des sémaphores, une
boucle while est indispensable pour garantir l’accès à la section critique pour des
raisons de indéterminisme d’accès au processeur.

Listing 3.7 – Classe Buffeur


1 //
2 // Buffer partage par tous acteurs du probleme
3 //
4 class Buffer {
5
6 protected final int max; // taille du buffer
7 protected final Object[] data; // le buffer est un tableau d’objets
8 protected int tete = 0; // indice dans le tableau pour l’ajout
9 protected int queue = 0; // indice dans le tableau pour consommer
10 protected int count = 0; // nb de cases occupees dans le buffer

107
Synchronisation en Java (LP)

11
12 public Buffer ( int max ){
13
14 [Link] = max;
15 [Link] = new Object[max];
16 }
17
18 public synchronized void put ( Object item ) {
19
20 while ( count == max ) {
21 try {
22 wait ();
23 }
24 catch ( InterruptedException e ) {
25 [Link]("InterruptedException");
26 }
27 }
28
29 data [ tete ] = item ;
30 tete = ( tete + 1 ) % max ;
31 count = count + 1 ;
32 notify () ;
33 }
34
35 public synchronized Object get () {
36
37 while ( count == 0 ) {
38 try {
39
40 wait () ;
41 } catch ( InterruptedException e ){
42 [Link]("InterruptedException"); }
43 }
44
45 Object result = data [ queue ] ;
46 queue = ( queue + 1 ) % max ;
47 count = count - 1 ;
48 notify () ;
49
50 return result;
51 }
52 }

Listing 3.8 – Classe Producteur


1 class Producteur extends Thread {
2
3 protected final Buffer buffer;
4
5 public Producteur ( Buffer buffer ) {
6
7 [Link] = buffer ;
8 }

108
Synchronisation en Java (LP)

9
10 public void run () {
11 try{
12
13 for ( int j = 0 ; j < 100 ; j ++ ){
14 [Link] ( 50 ) ;
15 [Link] ( new Integer ( j ) ) ;
16 [Link]("Producteur : " + j + " done");
17 }
18 } catch ( InterruptedException e ) {
19 [Link]("InterruptedException");
20 }
21 return ;
22 }
23 }

Listing 3.9 – Classe Consommateur


1 class Consommateur extends Thread {
2
3 protected final Buffer buffer;
4
5 public Consommateur ( Buffer buffer ) {
6
7 [Link] = buffer ;
8 }
9
10 public void run () {
11
12 try{
13 for ( int j = 0 ; j < 100 ; j ++ ){
14
15 Integer p = ( Integer ) ( [Link] () ) ;
16 int k = [Link] () ;
17 [Link] ( 100 ) ;
18 [Link]("Consommateur : " + j + " done");
19 }
20 } catch ( InterruptedException e ) {
21 [Link]("InterruptedException");
22 }
23 return ;
24 }
25 }

Listing 3.10 – Programme principal


1 class ProducteurConsommateur{
2
3 // tampon partage en lecture/ecriture
4 static Buffer buffer = new Buffer ( 40 ) ;
5
6 public static void main ( String [] args ) {
7

109
Synchronisation en Java (LP)

8 Producteur p = new Producteur ( buffer ) ;


9 Consommateur c = new Consommateur( buffer ) ;
10
11 [Link] () ;
12 [Link] () ;
13
14 try{
15 [Link]();
16 [Link]();
17 } catch ( InterruptedException e ) {
18 [Link]("InterruptedException");
19 }
20 [Link]("End");
21 }
22 }

Il faut noter qu’ici nous avons soit le buffeur vide, et les threads sont bloqués dans
la fonction wait() attendent pour réaliser un get(), soit ils attendent pour réaliser
un put(). Deux cas étant distincts il n’y a donc pas de risque d’utiliser le même verrou
pour réaliser l’attente.
En modifiant les temps d’attente chez le producteur et chez le consommateur nous
pouvons obtenir les différents types de blocage, soit du côté du producteur à cause
d’un buffeur plein (ce qui est le cas actuellement), soit du côté consommateur à cause
d’un buffeur vide. Vous pouvez également modifier le programme principal pour voir
si la solution proposée supporte une exécution avec plusieurs producteurs et plusieurs
consommateurs.

Correction Exercice 4 : Écriture d’un programme Java du problème des lecteurs


/ rédacteurs (priorité à la lecture)

Solution question 4.1 :


➽ Même remarque que pour l’exercice précédent à propos de l’absence d’utilisation
de sémaphores et l’utilisation des mutex. La solution proposée ici ne gère pas le pro-
blème de la famine des rédacteurs. En effet, si les lecteurs se groupent indéfiniment,
les rédacteurs n’auront pas la main car la contrainte d’accès en réécriture est très forte.
La trace du programme ne montre pas très bien ce problème. En effet, le nombre de
lectures des 5 lecteurs étant borné à 5 lectures et le nombre de rédactions des 5 ré-
dacteurs étant fixé au nombre de 2. Les rédactions se noient parmi les lectures. Par
contre, si on augmente le nombre de rédactions, celles-ci ont majoritairement lieu en
fin d’exécution.

Listing 3.11 – classe possedant les donnees a lire ou a modifier


1 class Journal {
2 public static final int PAUSE = 5;

110
Synchronisation en Java (LP)

3 private int nbLecteurs ;


4 private boolean Lecture ; // indique que la base est en lecture
5 private boolean Redaction ; // indique que la base est en ecriture
6
7 public Journal () {
8 nbLecteurs = 0 ;
9 Lecture = false ;
10 Redaction = false ;
11 }
12
13 public synchronized int demandeLecture () {
14 while (Redaction == true) {
15 try{
16 wait();
17 }
18 catch(InterruptedException e) { ... }
19 }
20 nbLecteurs ++ ;
21
22 // passage en mode lecteur par le premier lecteur en cours
23 if (nbLecteurs == 1)
24 Lecture = true ;
25 return nbLecteurs ;
26 }
27
28 public synchronized int finLecture () {
29 nbLecteurs -- ;
30
31 // liberation du journal dans son mode lecture
32 if (nbLecteurs == 0)
33 Lecture = false ;
34 notifyAll(); // reveille les redacteurs en attente
35 return nbLecteurs ;
36 }
37
38 public synchronized void demandeRedaction () {
39 while ( Lecture == true || Redaction == true ) {
40 try{ wait () ;
41 }
42 catch(InterruptedException e) { ... }
43 }
44 // passage en mode redaction des le premier acces (exclusif)
45 Redaction = true ;
46 }
47
48 public synchronized void finRedaction() {
49 Redaction = false ;
50 notifyAll () ; // reveille tous les lecteurs ou redacteurs
51 }
52
53 // simulation du temps de travail des acteurs du programme
54 public static void travaille(){
55 int tpsPause = (int) ( PAUSE * [Link] () ) ;
56 try {

111
Synchronisation en Java (LP)

57 [Link] ( tpsPause * 100 ) ;


58 }
59 catch(InterruptedException e) { ... }
60 }
61 }

Listing 3.12 – Classe Lecteur


1 class Lecteur extends Thread {
2 private int nbLectures = 5;
3 private Journal canard ;
4
5 public Lecteur ( Journal feuilleDeChou ) {
6 canard = feuilleDeChou ;
7 }
8
9 public void run () {
10 int nbLecteurs ;
11 while ( nbLectures > 0 ) {
12 [Link] () ;
13 nbLecteurs = [Link] () ;
14 [Link]( [Link]().getName() +
15 " lit ... " +
16 nbLecteurs + " lecture en cours" ) ;
17 [Link] () ;
18 nbLecteurs = [Link] () ;
19 nbLectures -- ;
20 [Link]( [Link]().getName() +
21 " ne lit plus ..." +
22 nbLecteurs + " lectures en cours" ) ;
23 }
24 }
25 }

Listing 3.13 – Classe Redacteur


1 class Redacteur extends Thread {
2 private int nbRedactions = 2;
3 private Journal canard ;
4
5 public Redacteur ( Journal feuilleDeChou ) {
6 canard = feuilleDeChou ;
7 }
8
9 public void run () {
10 while ( nbRedactions > 0 ) {
11 [Link] () ;
12 [Link] () ;
13 [Link]( [Link]().getName() +
14 " ecrit ..." ) ;
15 [Link] () ;
16 [Link] () ;
17 nbRedactions -- ;

112
Synchronisation en Java (LP)

18 [Link]( [Link]().getName() +
19 " n’ecrit plus ..." ) ;
20 }
21 }
22 }

Listing 3.14 – Programme principal


1 class LecteurRedacteur {
2
3 static Journal estRepublicain = new Journal ( ) ; // journal en
lect/ecrit
4
5 public static void main ( String [] args ) {
6 // tableau de lecteurs et redacteurs
7 Redacteur tabW [] = new Redacteur [ 5 ] ;
8 Lecteur tabR [] = new Lecteur [ 5 ] ;
9
10 for (int i = 0 ; i < 5 ; i ++ ) {
11 tabW[ i ] = new Redacteur ( estRepublicain ) ;
12 tabR[ i ] = new Lecteur ( estRepublicain ) ;
13 }
14
15 for (int i = 0 ; i < 5 ; i ++ ) {
16 tabW [ i ].start () ;
17 tabR [ i ].start () ;
18 }
19 }
20 }

➽ Trace d’exécution :

fenice[SynchroJava]% java LecteurRedacteur


Thread-0 ecrit ...
Thread-0 n’ecrit plus ...
Thread-4 ecrit ...
Thread-4 n’ecrit plus ...
Thread-2 ecrit ...
Thread-2 n’ecrit plus ...
Thread-7 lit ... 1 lecture en cours
Thread-9 lit ... 2 lecture en cours
Thread-1 lit ... 3 lecture en cours
Thread-3 lit ... 4 lecture en cours
Thread-5 lit ... 5 lecture en cours
Thread-1 ne lit plus ...4 lectures en cours
Thread-7 ne lit plus ...3 lectures en cours
Thread-5 ne lit plus ...2 lectures en cours
Thread-3 ne lit plus ...1 lectures en cours
Thread-1 lit ... 2 lecture en cours
Thread-9 ne lit plus ...1 lectures en cours
Thread-3 lit ... 2 lecture en cours
Thread-3 ne lit plus ...1 lectures en cours
Thread-5 lit ... 2 lecture en cours
Thread-5 ne lit plus ...1 lectures en cours
Thread-5 lit ... 2 lecture en cours
Thread-7 lit ... 3 lecture en cours

113
Synchronisation en Java (LP)

Thread-7 ne lit plus ...2 lectures en cours


Thread-1 ne lit plus ...1 lectures en cours
Thread-9 lit ... 2 lecture en cours
Thread-5 ne lit plus ...1 lectures en cours
Thread-5 lit ... 2 lecture en cours
Thread-9 ne lit plus ...1 lectures en cours
Thread-3 lit ... 2 lecture en cours
Thread-9 lit ... 3 lecture en cours
Thread-7 lit ... 4 lecture en cours
Thread-1 lit ... 5 lecture en cours
Thread-1 ne lit plus ...4 lectures en cours
Thread-5 ne lit plus ...3 lectures en cours
Thread-3 ne lit plus ...2 lectures en cours
Thread-7 ne lit plus ...1 lectures en cours
Thread-9 ne lit plus ...0 lectures en cours
Thread-8 ecrit ...
Thread-8 n’ecrit plus ...
Thread-0 ecrit ...
Thread-0 n’ecrit plus ...
Thread-6 ecrit ...
Thread-6 n’ecrit plus ...
Thread-4 ecrit ...
Thread-4 n’ecrit plus ...
Thread-2 ecrit ...
Thread-2 n’ecrit plus ...
Thread-1 lit ... 1 lecture en cours
Thread-9 lit ... 2 lecture en cours
Thread-5 lit ... 3 lecture en cours
Thread-7 lit ... 4 lecture en cours
Thread-3 lit ... 5 lecture en cours
Thread-3 ne lit plus ...4 lectures en cours
Thread-9 ne lit plus ...3 lectures en cours
Thread-3 lit ... 4 lecture en cours
Thread-1 ne lit plus ...3 lectures en cours
Thread-1 lit ... 4 lecture en cours
Thread-1 ne lit plus ...3 lectures en cours
Thread-7 ne lit plus ...2 lectures en cours
Thread-3 ne lit plus ...1 lectures en cours
Thread-5 ne lit plus ...0 lectures en cours
Thread-6 ecrit ...
Thread-6 n’ecrit plus ...
Thread-8 ecrit ...
Thread-8 n’ecrit plus ...
Thread-9 lit ... 1 lecture en cours
Thread-7 lit ... 2 lecture en cours
Thread-7 ne lit plus ...1 lectures en cours
Thread-9 ne lit plus ...0 lectures en cours
fenice[SynchroJava]%

Solution question 4.2 :


➽ Dans le problème des lecteurs / rédacteurs il n’est pas possible qu’un rédacteur
rédige alors qu’un lecteur lit. De plus, si le mode lecture est activé, toute demande
d’accès en lecture est accordée alors que toute demande de rédaction est mise en at-
tente. Ainsi, si un rédacteur accède finalement à la ressource, cela signifie que plus
aucun lecteur ne demande la ressource (un lecteur peut cependant être actif sans que
l’ordonnanceur ne lui ait encore donné la main pour qu’il fasse sa demande). Ainsi,
lorsqu’un rédacteur termine (libère la ressource) le nombre de rédacteurs en attente

114
Synchronisation en Java (LP)

est grand devant le nombre de lecteurs, puisque le nombre de rédacteurs demandant


à rédiger s’ajoute au nombre de rédacteurs mis en attente lorsque le mode lecture était
actif. Ce faisant, la probabilité que le prochain thread soit un thread rédacteur est plus
important.
Lorsqu’il y a une ou plusieurs lectures en cours, toute nouvelle demande de lecture
est accordée. Ainsi, les lecteurs passent devant les rédacteurs, leur nombre n’influence
en rien ce choix. Il y a en effet un risque de famine des rédacteurs.
Dans tous les cas, pour les deux raisons évoquées précédemment, il y a un phéno-
mène de grappe qui accompagne le mode lecture et le mode rédaction. Ce phénomène
est bien visible sur la trace du programme donné en correction à la question précé-
dente.

115
Deuxième partie

Système distribué

116
Chapitre 4

Introduction à la distribution (BH)

Contenu
4.1 Approche distribuée . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
4.1.1 Les systèmes centralisés : rappel . . . . . . . . . . . . . . . . . 118
4.1.2 Les systèmes distribués . . . . . . . . . . . . . . . . . . . . . . 118
4.1.3 Exemples de Systèmes Distribués . . . . . . . . . . . . . . . . 119
4.1.4 Une couche middleware . . . . . . . . . . . . . . . . . . . . . . 120
4.1.5 L’algorithmique distribuée . . . . . . . . . . . . . . . . . . . . 121
4.2 Modélisation des systèmes distribués . . . . . . . . . . . . . . . . . . 121
4.2.1 Les processus logiques . . . . . . . . . . . . . . . . . . . . . . . 121
4.2.2 La communication . . . . . . . . . . . . . . . . . . . . . . . . . 121
4.2.3 Le modèle d’exécution distribuée . . . . . . . . . . . . . . . . . 123
4.2.4 Le modèle algorithmique . . . . . . . . . . . . . . . . . . . . . 123
4.3 Evaluation des algorithmes distribués . . . . . . . . . . . . . . . . . 125
4.3.1 Performance d’un algorithme distribué . . . . . . . . . . . . . 125
4.3.2 Propriétés d’un algorithme distribué . . . . . . . . . . . . . . . 125
4.4 Exemple d’application distribuée . . . . . . . . . . . . . . . . . . . . 126
4.4.1 La gestion d’un garage de location de voitures . . . . . . . . . 126
4.4.2 Gestion de plusieurs compagnies de location de voitures . . . 128

Ce chapitre vise à introduire l’algorithmique distribuée. Pour cela nous introdui-


sons les systèmes distribués et les problématiques qu’ils engendrent afin de montrer ce
que l’algorithmique distribuée tend à résoudre. Nous présentons ensuite une propo-
sition de modélisation des systèmes distribués sur laquelle s’appuie la suite du cours.
Les propriétés des algorithmes distribués ainsi que la mesure de leur complexité sont
exposés dans une troisième section. Nous concluons ce chapitre par un exemple d’ap-
plication distribuée.

4.1 Approche distribuée

L’algorithmique centralisée (abordée jusqu’alors dans votre cursus) apporte des


solutions centralisées aux problèmes traités : les algorithmes proposés sont exécu-
tés sur une seule machine avec une seule instance de système d’exploitation qui gère

117
Introduction à la distribution (BH)

l’ensemble des ressources. On parle de distribution lorsque plusieurs ordinateurs tra-


vaillent ensemble. L’algorithmique distribuée apporte des solutions à des problèmes
où il y a plusieurs machines et donc plusieurs instances de systèmes d’exploitation,
chacun gérant ses propres ressources.

4.1.1 Les systèmes centralisés : rappel

Un système centralisé est composé d’un ensemble de processus s’exécutant sur


des mono-processeurs ou sur des multi-processeurs à mémoire partagée (multi-
processeurs, multi-cœurs).
Les propriétés d’un système centralisé sont les suivantes :
— Toutes les ressources (par exemple : mémoire, horloge) sont localisées sur la
même machine et accessibles par le programme
— Le système connaît l’état global de la machine et peut prendre des décisions en
fonction de cet état
— En cas d’accès concurrent, on utilise une programmation concurrente synchro-
nisée : sémaphore, mutex
— Exemple : problème de synchronisation en Java multi-thread.

4.1.2 Les systèmes distribués

Définition Nous définissons un système distribué ou réparti comme un ensemble


de composants (ordinateurs, téléphones, embarqués, ...) autonomes, interconnectés
par un réseau de communication, communiquant uniquement par échange de mes-
sages.
Les systèmes distribués sont caractérisés par les éléments suivants :
— Pas d’état global : un processus ne peut pas connaître instantanément l’état des
autres processus
— Pas d’horloge globale : chaque processus a sa propre horloge locale
— Échanges : communication et synchronisation, uniquement par échange de
messages
— Risque de perte de messages

Les apports des systèmes distribués Les systèmes distribués offrent l’accès et le par-
tage de ressources distantes, en effet, la distribution permet la mise à disposition de
services tels que :
— des ressources physiques comme les imprimantes, l’espace disque, les proces-
seurs, etc
— des ressources logiques comme des fichiers, des données textuelles, audio,
images, vidéo.

118
Introduction à la distribution (BH)

Ils gèrent la répartition géographique : un système de réservation avec des centres


dans différents lieux en est un exemple.
La connexion de machines en réseau permet d’obtenir une puissance de calcul à
moindre coût. De plus, la disponibilité est accrue avec les systèmes distribués. En effet,
on a une relative indépendance entre les défaillances des différents sites.
Enfin, les systèmes distribués permettent une certaine flexibilité car leur architec-
ture est plus modulaire.

Les difficultés des systèmes distribués La gestion de la distribution entraîne un cer-


tain nombre de difficultés car :
— il n’y a pas d’horloge globale
— il n’y a pas d’état global immédiat accessible à un site
— la fiabilité est relative : la distribution permet d’introduire de la tolérance aux
fautes
— la sécurité est relative : les architectures distribuées sont plus difficiles à proté-
ger du fait qu’il y a plusieurs points d’accès aux ressources, que l’architecture
évolue dynamiquement, etc.

4.1.3 Exemples de Systèmes Distribués

Nous pouvons regrouper les systèmes distribués en trois grandes classes : les sys-
tèmes de calculs distribués, les systèmes d’information distribués et les systèmes dis-
tribués "pervasive".

Les systèmes de calculs distribués Parmi les systèmes de calculs distribués, on dis-
tingue les clusters de calcul et les clouds/data centres.
Les clusters de calcul sont constitués d’un ensemble de nœuds identiques ayant le
même OS connectés par un réseau local haut débit et faible latence (ex : InfiniBand).
Cette architecture est caractérisée par une certaine homogénéité et un couplage fort
entre les ressources. Les clusters sont utilisés pour la programmation parallèle : un
même programme est exécuté en parallèle sur plusieurs machines (SPMD) pour du
calcul intensif par exemple.
Les clouds sont des systèmes qui profitent d’un ensemble de machines qui sont
ou ne sont pas dédiées. Ces machines peuvent être éloignées. Les architectures cloud
sont hétérogènes tant au niveau matériel que système. Les clouds sont utilisées pour
le calcul, le stockage de données, les applications, etc. Ils bénéficient de réseau haut
débit avec une latence plus élevée que les clusters.

Les systèmes d’information distribués Parmi ces systèmes d’information distri-


bués, on trouve les systèmes à base de transactions réparties (systèmes de gestion

119
Introduction à la distribution (BH)

de bases de données réparties), les EAI (Enterprise Application Integration) gérant la


coordination de différents systèmes, les midlewares et les services web.
Les systèmes de calculs distribués et les systèmes d’information distribués pré-
sentent une certaine stabilité car leurs noeuds sont fixes et fiables. Ils possèdent une
connexion permanente et de haute qualité.

Les systèmes mobiles et embarqués (”pervasifs”) Il s’agit des ordinateurs por-


tables, des téléphones, etc, mais aussi des robots des drônes ou des réseaux de cap-
teurs. Ces systèmes sont instables car les noeuds peuvent apparaître/disparaître et
sont mobiles. De plus, il n’y a pas toujours de connexion réseau.
Ces deux derniers types de systèmes seront peu abordés dans ce cours.

4.1.4 Une couche middleware

Environnement d’exécution distribuée Dans les systèmes distribués, on introduit


un environnement d’exécution distribuée, appelé couche middleware entre les sys-
tèmes d’exploitation locaux et les applications.
Les objectifs principaux de cet environnement sont d’offrir une interface permet-
tant de rendre indépendantes les applications par rapport aux plateformes et de cacher
la distribution aux utilisateurs en leur proposant une interface la plus proche possible
du "centralisé" .
Cette interface facilite l’accès et le partage des ressources distantes comme les im-
primantes, les ordinateurs, le réseau, les données et les fichiers.
De plus, cette interface vise à rendre transparente la distribution des ressources
aux utilisateurs. Pour cela, elle gère :
— la localisation : l’utilisateur ne sait pas où se trouve physiquement une res-
source
— la duplication des processus et/ou des données, par exemple en donnant un
même nom pour les différents dupliquas
— les problèmes de synchronisation liés à l’accès concurrent aux données
— la tolérance aux fautes en masquant les fautes.
Elle s’appuie sur des normes ISO comme la norme RM/ODP (Reference Model/Open
Distributed Processing).

Systèmes distribués ouverts et extensibles La couche middleware permet de déve-


lopper des systèmes distribués ouverts et extensibles.
Un système distribué ouvert offre des services en accord avec des standards dé-
crivant leur syntaxe et leur sémantique (IDL : Interface Definition Language). Il est
organisé en composants remplaçables et réutilisables. Il offre des interfaces en externe
vers les utilisateurs et les applications et en interne entre les différents composants.

120
Introduction à la distribution (BH)

Ces systèmes distribués sont extensibles en taille (augmentation du nombre de res-


sources et d’utilisateurs), géographiquement (augmentation de la distance entre les
utilisateurs et les ressources) et administrativement (différents modes d’administra-
tion).

4.1.5 L’algorithmique distribuée

L’algorithmique distribuée est l’ensemble des algorithmes gérant les problèmes


soulevés par les systèmes distribués et exécutés par le middleware "système distri-
bué". Dans la suite du cours, nous présentons une petite partie de ces algorithmes.

4.2 Modélisation des systèmes distribués

La représentation des systèmes distribués à l’aide de modèles conceptuels a pour


objectif d’en réaliser une description statique et comportementale à l’aide d’abstrac-
tions facilitant leur analyse et permettant de valider et prouver leurs propriétés.
Les éléments de modélisation sont :
— les processus, sites ou noeuds : notion de processus logique
— la communication : liens, topologie, protocoles (point à point, diffusion), etc.
— les connaissances partielles de chaque processus logique

4.2.1 Les processus logiques

Un processus logique est un élément logiciel effectuant une tâche donnée. Une tâche
est un ensemble d’instructions. L’exécution d’une instruction au sein d’un processus
génère un évènement.
Un évènement peut être :
— un changement d’état du processus,
— l’émission de message
— la réception de message
Un site logique possède : un identifiant unique, un état local, une mémoire locale,
une horloge locale et des procédures de communication.
Il connaît, par hypothèse, les processus avec qui il peut communiquer et le nombre
de processus de l’application.

4.2.2 La communication

La communication est le seul moyen d’interaction entre les processus, à la fois pour
échanger des données et pour se synchroniser. Ses propriétés sont donc importantes
dans la définition d’un algorithme distribué.

121
Introduction à la distribution (BH)

Communication synchrone et asynchrone Une communication synchrone entre


deux processus Pi et Pj se fait par rendez-vous, c’est-à-dire que le premier proces-
sus qui atteint le point de communication (ou rendez-vous) attend l’autre. A l’arrivée
du second processus au point de rendez-vous, les données sont transmises. Ensuite
chaque processus continue son exécution de manière indépendante. Il y a donc une
synchronisation entre l’émetteur et le récepteur.
Dans une communication asynchrone, le processus émetteur Pi envoie un message
et continue son exécution sans se préoccuper si le processus récepteur Pj l’a reçu. En
principe, on ne sait pas quand un message émis sera reçu. On suppose cependant que
les délais de communication sont finis.

Topologie La topologie d’un réseau décrit la façon dont les processus peuvent
communiquer entre eux. Elle peut être décrite par un graphe de communication où
les noeuds représentent les processus et où une arrête entre Pi et Pj dénote que Pi
peut communiquer directement avec Pj .

La topologie a une incidence sur la façon dont les processus peuvent communiquer
entre eux, et donc sur l’algorithme et le nombre de messages échangés :
— Diffusion : un seul message pour tous les autres processus,
— Sans fil/Graphes : ne peut pas contacter tous les processus, prévoir routage
— Anneau : limitation du nombre de messages, intéressant pour limiter la
consommation de batterie
— Arbre/Topologies régulières : routage dépendant de la topologie
La topologie utilisée par un algorithme peut être physique, si les processus sont
physiquement interconnectés suivant la topologie, ou virtuelle, si cette topologie est
définie logicielement au dessus de la topologie existante. Par exemple, Internet définit
une topologie de réseau complet virtuelle au dessus des réseaux physiques existants
(graphe), en permettant l’envoi direct de messages entre les processus qui s’y exé-
cutent. Par exemple, il est possible de définir une topologie d’anneau virtuel sur un
ensemble de processus numérotés de 1 à N, interconnectés suivant une topologie phy-
sique de réseau complet.

Modèles de communication Les deux protocoles de communication sont :


— le protocole Point à point : envoi d’un message à un destinataire explicite
— le protocole de Diffusion : envoi d’un même message à tous les destinataires ou
seulement à un groupe de destinataires
Selon le protocole, les fonctions de communication sont les suivantes :
— Point à point : envoi d’un message à un destinataire explicite
— Différents types de messages (ACK, REQ, etc.)
— Envoyer : Pi envoie T Y P E(parametres) à destinataire
— Recevoir : Pi reçoit T Y P E(parametres) de émetteur
— Diffusion : envoi d’un même message à tous les voisins

122
Introduction à la distribution (BH)

4.2.3 Le modèle d’exécution distribuée

Une exécution distribuée est caractérisée par les évènements qui s’y déroulent. Les
évènements considérés sont :
— les émissions de message
— les réceptions de message
— les évènements internes
Une exécution distribuée peut être modélisée de deux manières :
— par un chronogramme, comme cela est représenté sur la figure 4.1. C’est une
représentation graphique ordonnée des évènements, déroulée sur chaque pro-
cessus.
Processus 1 Processus 2 Processus 3

E1.1 E2.1

E1.2 E2.2
E2.3 E3.1
E2.4
E1.3
E2.5 E3.2
E3.3
E1.4 E3.4
E2.6
E1.5 E2.7 E3.5

E3.6
E1.6 E2.8
E3.7

E2.9 E3.8
E2.10
E1.7

F IGURE 4.1 – Exemple de chronogramme

— par un modèle d’évènements lorsque le nombre de processus et/ou la durée de


modélisation deviennent trop importants. L’exécution est généralement modé-
lisée sous la forme d’ensembles ordonnés d’évènements, un ensemble par pro-
cessus, et de messages échangés, un pour chaque couple de processus.
Attention, ces modèles donnent une représentation globale d’une exécution distri-
buée qui peut être utilisée pour avoir une vision globale de son comportement mais
aucun des processus n’a, en pratique, accès à cette représentation.

Restrictions des modèles de systèmes distribués Dans chaque modèle de systèmes


distribués, le nombre de processus est constant et la topologie de communication est
fixe. Les seules interactions possibles sont l’échange de messages et aucun processus
n’est isolé.

4.2.4 Le modèle algorithmique

Comme le modèle du système, le modèle algorithmique repose sur la notion de


processus et de communication. Par rapport aux modèles algorithmiques centralisés,
le modèle algorithmique distribué ajoute la notion de communication.

123
Introduction à la distribution (BH)

Algorithme exécuté par chaque processus Chaque processus exécute le même algo-
rithme décomposé en :
Données : les données locales au processus, avec éventuellement une valeur d’ini-
tialisation
Messages : les messages utilisés (envoyés/reçus) par le processus, ils peuvent
contenir des données si besoin
Règles : les fonctions ou tâches du processus. Il y en a une par évènement du
système. De ce fait, nous avons généralement les règles suivantes :
— Règle d’initialisation,
— Règles d’évènements internes, par exemple, demande ou sortie de section
critique.
— Règles de réception des messages, il y en a forcément une par type de mes-
sage défini.
En général on fait l’hypothèse du traitement atomique des règles, au moins sur les
règles de réception des messages, pour éviter d’avoir à traiter la concurrence interne.

Modèle de communication Comme dit précédemment les algorithmes distribués


ajoutent la notion de communication aux instructions possibles. Les fonctions de com-
munication sont généralement supposées asynchrones. Elles sont écrites selon le mo-
dèle suivant :
— l’envoi d’un message a la forme suivante :
émetteur envoie TYPE_MESSAGE(paramètres) à destinataire.
Les données du messages sont transmises dans les paramètres.
— pour la réception nous définissons des règles différentes suivant le type de mes-
sage reçu :
récepteur reçoit TYPE_MESSAGE(paramètres) de émetteur
La synchronisation dépend des propriétés des fonctions de communication. Dans
ce cours, on considère des communications ayant les propriétés suivantes :
— les messages sont typés : un type est associé à chaque message.
— il n’y a pas de buffeurisation, les règles associées aux messages sont appliquées
immédiatement, dès la réception des messages.
— les règles sont exécutées de manière événementielle : la règle est déclenchée
lorsque l’évènement survient.
— la fonction d’envoi n’est pas bloquante
Les messages échangés sont souvent des messages de contrôle, il y a donc diffé-
rents types de messages, par exemple message de requête, message d’acquittement,
etc.

Exemple d’algorithme distribué L’algorithme suivant illustre la diffusion d’un mes-


sage dans un anneau.

124
Introduction à la distribution (BH)

Algorithme 42 : Algorithme de diffusion d’un message dans un anneau


Variables pour chaque processus Pi :
Succi : initialisé au successeur de Pi dans l’anneau
Messages utilisés :
BROADC(Pi , Msg) : Message de diffusion
diffusion initiée par Pi
contenu du message diffusé : M sg
Règle 1 : Pi demande de diffuser Msg sur l’anneau
début
Pi envoie BROADC(Pi , Msg) à Succi
fin
Règle 2 : Pi reçoit BROADC(init, Msg) de Pj
début
if init !=i then
Pi envoie BROADC(init, Msg) à Succi
end if
fin

Il est important de rappeler que, dans un système distribué, chaque processus pos-
sède ses propres données qui ne sont pas partagées avec les autres. Ainsi, dans l’algo-
rithme 42, chaque processus de l’anneau possède sa propre instance de variable Succi ,
différente de celle des autres processus.

4.3 Evaluation des algorithmes distribués

4.3.1 Performance d’un algorithme distribué

Comme pour les algorithmes centralisés, on recherche à évaluer l’efficacité :


— locale au processus : en évaluant la complexité classique de l’algorithme
— la complexité distribuée :
— le nombre de processus impliqués qui a une incidence sur la charge des
processeurs
— le nombre de messages qui correspond au nombre de messages requis par
un algorithme distribué. Cette complexité a une incidence sur la charge du
réseau ou la consommation énergétique.

4.3.2 Propriétés d’un algorithme distribué

Lors de la conception d’algorithmes distribués, il est nécessaire de prouver leur


bon fonctionnement. Pour cela, il est possible d’évaluer les propriétés suivantes :

125
Introduction à la distribution (BH)

— la propriété de sûreté : un état catastrophique ne sera jamais atteint. Par


exemple, pour un algorithme qui garantit l’exclusion mutuelle, la section cri-
tique doit être accessible par au plus un processus.
— la propriété de vivacité : un état de satisfaction sera fatalement atteint. Par
exemple, toute demande d’accès à la section critique sera satisfaite ou encore
tout message émis sera reçu.
— la propriété d’ équité : tous les processus ont les même chances d’accès à une
ressource. Par exemple, il n’y a pas de processus qui accède plus souvent à une
section critique.
— la propriété de terminaison : l’algorithme distribué se termine ou conduit à une
décision. Par exemple, l’élection d’un processus maître .
— la propriété de ponctualité : Des contraintes temporelles peuvent être fixées Par
exemple : une échéance de remise de messages au plus tard.

4.4 Exemple d’application distribuée

Nous présentons dans la suite l’exemple de gestion distribuée d’un garage de lo-
cation de voiture. Cet exemple sert à illustrer certains problèmes pouvant être gérés
avec l’algorithmique distribuée.

4.4.1 La gestion d’un garage de location de voitures

Présentation du problème La capacité du garage de location de voitures est limitée


à N places. Les voitures louées ne sont pas forcément restituées à ce garage et les
voitures restituées ne sont pas forcément issues de ce garage.
Il y a 2 guichets d’accès au garage géographiquement distants l’un de l’autre :
— un guichet de départ : pour la prise de voitures qui gère l’opération Louer ;
— un guichet de retour : pour les restitutions de voitures qui gère l’opération Res-
tituer.

Contrôle des mouvements de voitures L’application doit gérer le mouvement des


voitures, c’est-à-dire leur location et leur restitution.
La propriété de sûreté doit être garantie, ce qui implique :
— qu’il ne doit pas y avoir trop de voitures dans le garage (pas plus de N voi-
tures) ;
— que l’on ne peut pas louer plus de voitures qu’il n’en existe.
Pour gérer cette propriété, on introduit deux compteurs :
— le compteur du nombre de voitures louées, L, géré par le guichet de départ ;
— le compteur du nombre de voitures restituées, R, géré par le guichet d’arrivée.

126
Introduction à la distribution (BH)

La propriété de sûreté se traduit par l’invariant : 0 ≤ (R − L) ≤ N .

Problème : chaque guichet distants l’un de l’autre a besoin de connaître les deux
compteurs pour pouvoir prendre des décisions.

Solution 1 : contrôle centralisé Idée : centraliser la gestion des variables L et R. Pour


cela, on introduit un site hébergeant un serveur central qui fournit un service contrôle
offrant deux primitives :
— Incremente(R)
— Incremente(L)
Chaque fois que les guichets départ et retour gèrent une voiture, ils doivent faire appel
au serveur central.
Problème : la disponibilité du service repose sur la disponibilité des 3 sites.

Solution 2 : duplication des compteurs Idée : les compteurs L et R sont dupliqués.


Ainsi le guichet départ possède une copie du compteur R et le guichet retour possède
une copie du compteur L. L’application doit s’appuyer sur un protocole de gestion des
copies des compteurs.
Ce protocole doit maintenir les propriétés suivantes :
— le compteur "copie" a initialement la même valeur que le compteur "source",
— le compteur "copie" ne peut prendre que les mêmes valeurs que le compteur
"source" et dans le même ordre chronologique,
— le compteur "copie" peut ne pas prendre toutes les valeurs du compteur
"source",
— si le compteur prend la valeur k, le compteur "copie" finira par prendre une
valeur égale ou supérieure à k.
Problème : concevoir des algorithmes qui permettent de gérer la cohérence des
copies de compteurs.

Solution 3 : distribution par circulation Idée : simuler des variables globales des
compteurs en faisant circuler entre les guichets un unique message contenant la valeur
courante de ces compteurs. Ce message, appelé jeton, sera circulant et valué.
Quand un guichet veut effectuer une opération, il doit détenir le jeton. Quand un
guichet possède le jeton :
— il a le droit d’utiliser les compteurs reçus,
— il possède la valeur courante de ces compteurs.

127
Introduction à la distribution (BH)

4.4.2 Gestion de plusieurs compagnies de location de voitures

Présentation du problème Il existe plusieurs compagnies de voitures qui partagent


le même garage. Ces différentes compagnies ne mettent pas en commun leurs sys-
tèmes d’information. Chaque compagnie possède un guichet de départ et un guichet
de retour et ne loue que ses propres voitures. S’il y a C compagnies, il y a C compteurs
Li et C compteurs Ri répartis dans chaque guichet (chaque guichet i possède ainsi un
compteur Li et un compteur Ri ).

Comment gérer cette distribution ?


— Location : la connaissance du compteur local à la compagnie est suffisante, il
faut gérer la mise à jour des copies pour les autres compagnies
— Restitution : il faut connaître tous les compteurs de toutes les compagnies.

128
Chapitre 5

Synchronisation entre processus


dans les systèmes distribués (BH)

Contenu
5.1 Synchronisation d’horloges . . . . . . . . . . . . . . . . . . . . . . . . 129
5.1.1 Exemple : absence d’une heure unique sur le programme
make de UNIX . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
5.1.2 Notion d’horloges logiques . . . . . . . . . . . . . . . . . . . . 131
5.1.3 Notion d’horloges physiques . . . . . . . . . . . . . . . . . . . 136
5.2 Synchronisation dans les systèmes distribués . . . . . . . . . . . . . 137
5.3 Synchronisation distribuée à l’aide d’un coordinateur : méca-
nismes de synchronisation centralisés . . . . . . . . . . . . . . . . . 137
5.4 Synchronisation distribuée utilisant la notion d’horloges logiques 139
5.4.1 Algorithme d’exclusion mutuelle de Lamport . . . . . . . . . 140
5.4.2 Algorithme d’exclusion mutuelle de Ricart et Agrawala . . . 143
5.4.3 Algorithme d’exclusion mutuelle de Carvalho et Roucairol . . 145
5.5 Synchronisation distribuée utilisant un jeton . . . . . . . . . . . . . 147
5.5.1 Algorithme d’exclusion mutuelle de Le Lann . . . . . . . . . . 147
5.5.2 Algorithme d’exclusion mutuelle de Suzuki et Kasami . . . . 148
5.6 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
5.7 Correction des Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . 150

La synchronisation dans les systèmes distribués est réalisée à l’aide d’algorithmes


distribués. En effet, les processus ne partageant pas de mémoire, les informations
concernant le système sont réparties, il n’est donc pas possible de prendre une dé-
cision en ayant la vue de toutes celles-ci.

5.1 Synchronisation d’horloges

Dans un système centralisé, il n’y a pas ambiguïté pour dater des évènements ou
des objets. Quand un processus veut connaître la date courante, il fait un appel sys-
tème et le noyau la lui donne. Sur une même machine, si un processus P1 demande la

129
Synchronisation entre processus dans les systèmes distribués (BH)

date et un peu plus tard un processus P2 la demande, alors la valeur acquise par P2
sera supérieure ou égale à celle acquise par P1 .
Dans un système distribué, on n’est pas sûr d’obtenir ce comportement. En effet, il
est difficile aux processeurs d’être tous d’accord sur l’heure, en effet, chaque machine
dispose d’une horloge indépendante, avec un réglage initial différent ... Il y a donc des
risques d’ambiguïté.
Dans l’exemple suivant, le scénario de l’exécution d’un programme make en distri-
bué est présenté pour illustrer les problèmes soulevés par l’absence d’horloge globale
dans un système distribué.

5.1.1 Exemple : absence d’une heure unique sur le programme make de


UNIX

Sous UNIX, les applications composées de longs programmes sont découpées en


plusieurs fichiers sources. Toute modification de l’un de ces fichiers requiert la compi-
lation de celui-ci et non pas la compilation de l’ensemble des fichiers sources.

Fonctionnement de make en système centralisé Si un programmeur modifie un ou


plusieurs fichiers sources, il lance la compilation de son application avec make. Le
programme make ne compile que les fichiers sources qui ont été modifiés depuis la
dernière compilation. Pour savoir quels fichiers sources ont été modifiés, il compare
les dates de modification des fichiers sources et objets correspondants.
Par exemple, si la date de dernière modification du fichier source fichA.c est
1110 et celle du fichier objet correspondant fichA.o est 1100, make détecte que le
fichier fichA.c a été modifié, il doit être recompilé. Par contre, si la date de dernière
modification du fichier source fichB.c est 1120 et celle du fichier objet correspon-
dant fichB.o est 1130, alors fichB.c n’est pas compilé. Le programme make vérifie
tous les fichiers sources de l’application pour savoir ceux qui doivent être compilés et
lancent leur compilation.

Simulation du comportement de make en système distribué Dans le cas d’un sys-


tème distribué, il pourrait se trouver que l’éditeur de texte et le compilateur permet-
tant de développer l’application ne s’exécutent pas sur les mêmes processeurs : les
horloges de ces processeurs ne sont pas forcément synchrones. Supposons que la date
de dernière modification de fichA.o soit 1110 et que peu de temps après fichA.c
soit modifié à la date 1108 car l’horloge de sa machine est un peu en retard. Dans ce
cas, le programme make ne détecte pas que fichA.c a été modifié : il ne sera pas re-
compilé. Le programme binaire de l’application est composé d’un mélange de fichiers
objets provenant d’anciennes et de nouvelles sources. Il ne fonctionnera pas comme
souhaité ou pas du tout. . .

130
Synchronisation entre processus dans les systèmes distribués (BH)

À la suite de cet exemple, nous voyons qu’il est important de se poser la question
suivante :
Peut-on synchroniser toutes les horloges d’un système distribué pour ob-
tenir une horloge unique, standard et non ambiguë ?

5.1.2 Notion d’horloges logiques

Dans un article célèbre (1978), Lamport montre que la synchronisation d’horloges


est possible et propose un algorithme qui permet de la réaliser. Il montre que la syn-
chronisation ne doit pas forcément être absolue, en effet :
— Dans le cas de où 2 processus n’ont aucune interaction, il n’est pas nécessaire
que leurs horloges soient synchrones.
— Le problème n’est pas que tous les processus aient la même date mais qu’ils
soient d’accord sur l’ordre dans lequel les évènements se produisent. Pour cela,
la notion d’horloges logiques est introduite.

Problématique : Comment ordonner les évènements pour qu’à chaque évènement


E soit associé une horloge H(E) sur laquelle tous les processus soient d’accord ? Cette
horloge doit vérifier la propriété suivante :
Soit deux évènement E1 E2 , si E1 a lieu avant E2 alors H(E1 ) < H(E2 )

Relation de précédence Lamport introduit la relation de précédence ou "happens-


before" qui permet d’ordonner les événements d’une application distribuée. L’ordre
obtenu est un ordre partiel. La relation de précédence est notée : →
L’expression E1 → E2 , se lit l’événement E1 précède l’événement E2 , c’est-à-dire que
E1 a lieu avant E2 .

La relation de précédence entre deux évènements E1 et E2 peut s’observer dans


les cas suivants :
— les deux évènements ont lieu dans un même processus
— E1 consiste en l’émission d’un message par un processus et E2 sa réception par
un autre processus : dans ce cas, la relation peut aussi être appelée relation de
Causalité.

La relation de précédence définit une relation d’ordre car elle satisfait les propriétés
suivantes :
— transitivité : si E1 → E2 et E2 → E3 alors E1 → E3
— irréflexibilité : on n’a pas E1 → E1
— antisymétrie : si (E1 → E2 ) alors ¬(E2 → E1 )

131
Synchronisation entre processus dans les systèmes distribués (BH)

Remarques sur la relation de précédence :


— La relation de précédence détermine un ordre partiel entre les évènements :
il n’est pas toujours possible de mettre en relation de précédence deux évène-
ments. Par exemple, nous pouvons avoir ¬(E1 → E2 ) et ¬(E2 → E1 )
— E1 ↛ E2 signifie que les événements E1 et E2 ne dépendent ni directement ni
transitivement l’un de l’autre.
— Dans ce cas E2 n’est pas affecté par E1 ni aucun autre événement qui a lieu
après E1 dans le même processus
— On peut noter :
— ∀E1 , E2 : E1 ↛ E2 ⇏ E2 ↛ E1
— ∀E1 , E2 : E1 → E2 ⇒ E2 ↛ E1

Exercice 1 : Relations de précédence


Écrire les relations de précédence entre les évènements des trois processus du chrono-
gramme 5.1.

Processus 1 Processus 2 Processus 3

E1.1 E2.1

E1.2 E2.2
E2.3 E3.1
E2.4
E1.3
E2.5 E3.2
E3.3
E1.4 E3.4
E2.6
E1.5 E2.7 E3.5

E3.6
E1.6 E2.8
E3.7

E2.9 E3.8
E2.10
E1.7

F IGURE 5.1 – Ensemble d’évènements

132
Synchronisation entre processus dans les systèmes distribués (BH)

Relation de concurrence Deux évènements E1 et E2 sont concurrents si et seulement


V
si, ils n’ont pas de relation de précédence : (E1 ↛ E2 ) (E2 ↛ E1 ), ce qui est noté
E1 ∥ E2
Remarques sur la relation de concurrence :
— La relation de concurrence n’est pas transitive E1 ∥ E2 et E2 ∥ E3 ⇏ E1 ∥ E3
— ∀E1 , E2 nous avons E1 → E2 ou E2 → E1 ou E1 ∥ E2
— Des suites d’évènements avec précédence peuvent être concurrentes :
E13 → E14 → E15 ∥ E33 → E34
— La concurrence “logique” ne signifie pas que les événements sont exécutés en
même temps contrairement à la concurrence “physique”
— Deux événements concurrents ne peuvent pas se produire au sein du même
processus.

Horloges logiques Reposant sur la relation de précédence, Lamport introduit la no-


tion d’horloges logiques ou estampilles. Chaque processus gère sa propre horloge lo-
gique de la manière suivante :
— une horloge logique est une valeur entière
— suite à un évènement local, l’horloge logique est augmentée de 1
— Suite à un envoi, l’horloge logique est augmentée de 1
— Suite à une réception, l’horloge logique est fixée à :
M ax(Hlocale , Hreceptionnee ) + 1

Exemple illustrant la proposition de Lamport : Considérons l’exemple de la fi-


gure 5.2, les processus s’exécutent sur des machines différentes, chacune avec sa
propre horloge, ayant chacune sa propre vitesse. Prenons comme hypothèse que
lorsque l’horloge de P0 a avancé de 2 temps, celle de P1 a avancé de 5 et celle de
P2 de 10.
— Comportement sans l’algorithme de Lamport (figure 5.2)
À la date 2, P0 envoie un message à P1 . La date de P1 est de 10 à la réception
du message. Si le message transporte la date d’émission, 2, P1 conclut qu’il a
fallu 8 tops au message pour arriver. Cela est possible. En suivant le même
raisonnement, le message B de P1 vers P2 a mis 25 tops pour arriver. Ceci est
également possible.
Par contre, le message C de P2 à P1 quitte P2 à 50 et arrive chez P1 à 30. De
même, le message D quitte P1 à 35 et arrive chez P0 à 16. Ces valeurs sont
clairement impossibles. Ce sont ces évènements que l’on veut éviter.
— Comportement avec l’algorithme de Lamport (figure 5.3)

La solution de Lamport suit la relation de précédence. Regardons ce que cela


donne sur cet exemple. Si P2 envoie le message C à 50, celui-ci doit arriver
à 51 ou plus. Chaque message transporte la date d’émission du point de vue
de l’émetteur. Quand un message arrive et que le récepteur voit que la date

133
Synchronisation entre processus dans les systèmes distribués (BH)

0 0 0

2 message A 5 10

4 10 20

6 15 message B 30

8 20 40

10 25 50
message C

12 30 60

14 35 70
message D

16 40 80

P0 P1 P2

F IGURE 5.2 – Échange de messages : sans Lamport

0 0 0

2 message A 5 10

4 10 20

6 15 message B 30

8 20 40

10 25 50
message C
Max(50,30)+1
12 30 60
51

14 56 35 70
message D
Max(56,16)+1
16 57 61 40 80

P0 P1 P2

F IGURE 5.3 – Échange de messages : avec Lamport

134
Synchronisation entre processus dans les systèmes distribués (BH)

d’émission est supérieure à sa date de réception, alors il passe son horloge à


(horloge reçue+1). Ainsi, les horloges véhiculées sont des horloges logiques.

Estampilles et ordre total Rappelons qu’avec un ordre total :


— Tous les événements peuvent être comparés deux à deux par la relation d’ordre
R
— ∀E1 , E2 : E1 RE2 ∨ E2 RE1

Horloges logiques et ordre total :


— La comparaison entre horloges de deux évènements est possible s’il existe une
relation de précédence entre les évènements : E1 → E2 alors H(E1 ) < H(E2 )
— Mais deux, ou plusieurs, événements concurrents peuvent avoir la même va-
leur d’horloge
— Ainsi les horloges logiques seules ne peuvent pas servir de support à un pro-
blème de décision :il est nécessaire d’avoir un mécanisme supplémentaire pour
mettre en place un ordre total
— Chaque processus dispose d’un identificateur unique
— Les identificateurs de processus sont associés aux estampilles qui deviennent
des couples : (Hi , Pi ), Hi est l’horloge locale, Pi l’identificateur du processus
— En cas d’égalité d’estampille, l’horloge la plus petite est celle du processus
ayant le plus petit identificateur.
— Ainsi pour deux événements E1 et E2 , d’estampille H1 et H2 ayant lieu sur les
processus Pi et Pj on a :
H(E1 ) < H(E2 ) ⇔ H1 < H2 ∨ (H1 = H2 ∧ i < j)

Conclusion En utilisant l’ordonnancement proposé par Lamport, il est possible d’at-


tribuer une horloge logique ou estampille à tous les évènements d’un système distri-
bué. Cette horloge logique est conforme aux conditions suivantes :
1. Si un événement E1 précède un événement E2 dans le même processus, alors
H(E1 ) < H(E2 ).
2. Si E1 et E2 représentent respectivement l’envoi et la réception d’un message,
alors H(E1 ) < H(E2 ).
3. pour tous évènements, E1 et E2 , H(E1 ) est différente de H(E2 )
Les horloges de Lamport permettent bien d’ordonner les événements d’un système
distribué.

135
Synchronisation entre processus dans les systèmes distribués (BH)

Utilisation des horloges logiques Les horloges logiques introduites par Lamport
sont utilisées dans de nombreux cas en algorithmique distribuée, nous en citons
quelques uns. La plupart de ces utilisations vont être présentées dans la suite du cours.
— algorithmes de mise en œuvre de files d’attente virtuelles réparties :
1. exclusion mutuelle répartie
2. mise à jour de copies cohérentes
3. diffusion cohérente
— détermination de l’événement le plus récent dans un ensemble d’événements :
1. gestion de la cohérence de caches
2. mise en œuvre de la mémoire virtuelle partagée
3. datation des transactions réparties
— génération de noms uniques
— synchronisation d’horloges physiques

Avantages des horloges logiques Nous citons trois principaux avantages :


— c’est la première datation répartie introduite
— elles sont économiques : la datation est réalisée par un seul nombre et non par
un vecteur (nous en verrons un exemple par la suite)
— la causalité des messages est respectée par remise à l’heure du récepteur.

Limitation de la datation par horloges logiques


— E1 → E2 implique H(E1 ) < H(E2 ) est une condition faible car on ne peut rien
affirmer si H(E1 ) < H(E2 )
— soit les évènements E1 et E2 sont concurrents
— soit les évènements E1 et E2 sont ordonnés
— Quand deux évènements sont concurrents, on ne peut rien conclure quant à
leurs horloges logiques respectives
— Seule certitude :
Si H(E1 ) = H(E2 ) Alors E1 et E2 sont concurrents.
— Les horloges logiques ne sont pas denses :
soient E1 et E2 tels que H(E1 ) < H(E2 ), on ne peut pas savoir s’il existe E3 tel
que E1 → E3 et/ou E3 → E2

5.1.3 Notion d’horloges physiques

L’algorithme de Lamport propose une solution pour ordonner les évènements de


façon non ambiguë. Les horloges logiques associées aux évènements ne correspondent
pas forcément à la date réelle à laquelle ils se produisent. Dans certains systèmes,
le temps réel est important. Il existe un certain nombre d’algorithmes permettant de
gérer ce problème, mais nous n’abordons pas cette problématique dans ce cours.

136
Synchronisation entre processus dans les systèmes distribués (BH)

5.2 Synchronisation dans les systèmes distribués

Comme nous l’avons déjà souligné, les mécanismes d’exclusion mutuelle ne


peuvent pas être implantés de la même façon dans les systèmes distribués que dans
les systèmes centralisés. En effet, les processus ne partagent pas de mémoire, ils ne
peuvent pas utiliser les outils présentés dans les systèmes centralisés pour se synchro-
niser comme les sémaphores par exemple.
En système distribué, nous présentons deux classes de mécanismes de synchroni-
sation : les mécanismes centralisés et les mécanismes distribués.
Les mécanismes de synchronisation centralisés se reposent sur un coordinateur
pour gérer l’exclusion mutuelle.
En ce qui concerne les mécanismes distribués, plusieurs approches sont proposées :
— algorithmes basés sur l’ordonnancement des évènements selon Lamport : dans
ce cas, chaque requête d’entrée en Section Critique possède une date qui permet
d’ordonner les requêtes.
— algorithmes basés sur la notion de jeton : un message ayant un format spéci-
fique, appelé jeton, circule entre les processus en suivant un anneau virtuel.
— algorithmes basés sur l’ordonnancement des évènements suivant des priorités :
les évènements sont ordonnés suivant la priorité associée aux processus et non
plus selon leur date. Ces algorithmes sont utilisés dans le cas de systèmes temps
réels proposant des processus avec priorités différentes.
Dans ce cours, seules les deux premières approches de mécanismes distribués sont
abordées.

5.3 Synchronisation distribuée à l’aide d’un coordinateur :


mécanismes de synchronisation centralisés

Nous appelons mécanismes centralisés de synchronisation en système distribué,


les algorithmes qui utilisent un coordinateur. Ce coordinateur centralise les requêtes
des différents processus de l’application et prend les décisions. Par exemple, pour
réaliser l’exclusion mutuelle il accorde la permission d’entrée en Section Critique. Il
gère une file des requêtes en attente de Section Critique.

137
Synchronisation entre processus dans les systèmes distribués (BH)

Algorithme de synchronisation distribuée à l’aide d’un coordinateur


— Quand un processus Pi veut entrer en Section Critique, il envoie au coordina-
teur un message de "demande d’entrée en Section Critique". S’il n’y a personne
en Section Critique, alors le coordinateur envoie un message de "permission
d’entrée en SC" à Pi . Quand Pi reçoit la permission du coordinateur, il entre en
Section Critique.
— Pendant ce temps, si un processus Pj demande l’accès à la Section Critique.
Le coordinateur ne peut pas autoriser l’accès. La méthode pour refuser l’accès
dépend de l’implantation :
— soit le coordinateur n’envoie pas de réponse, dans ce cas le processus Pj est
bloqué.
— soit il envoie une réponse : "Permission non accordée".
Dans les deux cas, le coordinateur dépose la demande de Pj dans une file d’at-
tente.
— Quand Pi quitte la Section Critique, il envoie un message de sortie de Section
Critique au coordinateur.
— À la réception de ce message, le coordinateur prend la première requête de
la file de processus en attente de la Section Critique et envoie au processus
qui a effectué cette requête (par ex Pj ) un message de permission d’entrée en
Section Critique. Si Pj est bloqué, il se débloque et entre en Section Critique.
Dans l’autre cas, il peut entrer en Section Critique dés qu’il reçoit la permission,
selon sa tâche actuelle.
Cette approche centralisée a des avantages et des inconvénients que nous présen-
tons à la suite.

Avantages
— cet algorithme garantit l’exclusion mutuelle, il est juste (les demandes sont ac-
cordées dans l’ordre de réception), il n’y a pas de famine (aucun processus ne
reste bloqué).
— elle est facile à implanter.
— elle nécessite au maximum 3 messages pour entrer en Section Critique (de-
mande de section critique, refus, accord).

Inconvénients
— si le coordinateur a un problème, tout le système s’effondre.
— si les processus sont bloqués quand ils demandent l’accès à une Section Cri-
tique occupée alors ils ne peuvent pas détecter la panne du coordinateur.
— dans de grands systèmes, le coordinateur peut vite devenir un goulet d’étran-
glement des performances.

Remarque : Ces critiques sont toujours valables lorsqu’on applique une gestion cen-
tralisée dans un système distribué.

138
Synchronisation entre processus dans les systèmes distribués (BH)

5.4 Synchronisation distribuée utilisant la notion d’horloges


logiques

Dans les algorithmes qui suivent, nous aurons les hypothèses suivantes :
— les processus communiquent uniquement par échange de messages
— chaque processus connaît l’ensemble des processus du système
— chaque processus a ses propres variables, il n’y a pas de partage de variables
entre processus
— les pertes de messages ne sont pas prises en compte, ni les pannes de machines
— les échanges de messages entre processus sont FIFO

Le principe général de ces algorithmes est le suivant :


— La demande d’entrée en SC est diffusée aux processus
— Pour accéder à la SC, il faut l’accord de tous les autres processus (consensus)
— La difficulté à résoudre est la gestion des demandes concurrentes d’entrée en
SC

139
Synchronisation entre processus dans les systèmes distribués (BH)

5.4.1 Algorithme d’exclusion mutuelle de Lamport

Cet algorithme utilise la notion d’horloges logiques pour ordonner les évènements.
Rappelons qu’un processus Pi incrémente son horloge Hi entre 2 évènements et que
à la réception d’un message d’horloge H, l’horloge Hi de Pi est recalée comme suit :
Hi = max(Hi , H) + 1.
Chaque processus maintient sa propre queue de requêtes qui n’est jamais consultée
par d’autres processus. La queue de requêtes contient initialement le seul message
(H0 : P0 demande de ressource), où P0 est le processus qui demande initialement la
Section Critique et H0 est plus petit que la valeur initiale de toutes les horloges.
Initialement, chaque processus connaît tous les autres participants.

Principe de l’algorithme
1. Un processus Pi qui veut entrer en Section Critique, incrémente son horloge et
envoie le message (Hi : Pi demande de SC) à tous les autres processus et met le
message dans sa queue de requêtes ; Hi est l’horloge associée au message.
2. Quand un processus Pi reçoit le message (Hj : Pj demande de SC), il synchro-
nise son horloge, met le message dans sa queue de requêtes et envoie un mes-
sage daté de bonne réception à Pj .
3. Pi accède à la SC quand les deux conditions suivantes sont réalisées :
(a) Il existe un message (Hi : Pi demande de SC) dans sa queue de requêtes
qui est ordonné devant tout autre requête dans sa queue selon la relation de
précédence.
(b) Le processus Pi a reçu un message de tous les autres processus ayant une
date supérieure à Hi .
Ces 2 conditions sont testées localement par Pi .
4. Quand Pi quitte la SC, il enlève la requête (Hi : Pi demande de SC) de sa queue
de requêtes, incrémente son horloge et envoie un message daté (Hi :Pi quitte
SC) à tous les processus.
5. Quand Pj reçoit le message (Hi : Pi quitte SC), il synchronise son horloge et
enlève le message (Hi : Pi demande de SC) de sa queue de requêtes.

Rappel : Si deux messages possèdent la même horloge, ce sera le numéro du site


qui départagera.

Algorithme d’exclusion mutuelle de Lamport Nous introduisons avec l’algo-


rithme 43 une présentation sous forme de règles : une règle correspond à un événe-
ment local ou distant. Les variables utilisées par un processus ne sont pas connues des
autres processus. Cet algorithme présente les règles exécutées par chaque processus
Pi de l’application. Les fonctions sont exécutées de façon atomique.

140
Synchronisation entre processus dans les systèmes distribués (BH)

Algorithme 43 : Algorithme d’exclusion mutuelle de Lamport


Variables utilisées par chaque processus Pi :
Hi : entier, init 0 // horloge locale
F _Hi [] : tableau d’entiers // horloges des processus
F _Mi [] : tableau d’états, ∈ {REQ, ACK, REL} // États des processus
Vi : ens. de processus, init voisins de Pi
Messages utilisés :
REQ(H) : demande d’entrée en SC
ACK(H) : acquittement d’une demande d’entrée en SC
REL(H) : sortie de SC
Règle 1 : Pi demande la Section Critique
begin
Hi ← Hi + 1
∀Pj ∈ Vi , Pi envoie REQ(Hi ) à Pj
F _Hi [i] ← Hi
F _Mi [i] ← REQ
Attendre ∀Pj ∈ Vi ((F _Hi [i] < F _Hi [j]) ∨ ((F _Hi [i] = F _Hi [j]) ∧ i < j))
<Section Critique>
end
Règle 2 : Pi reçoit REQ(H) de Pj
begin
Hi ← max(Hi , H) + 1
F _Hi [j] ← H
F _Mi [j] ← REQ
Pi envoie ACK(Hi ) à Pj
end
Règle 3 : Pi reçoit ACK(H) de Pj
begin
Hi ← max(Hi , H) + 1
if F _Mi [j] ̸= REQ then
F _Hi [j] ← H
F _Mi [j] ← ACK
end if
end
Règle 4 : Pi libère la Section Critique
begin
Hi ← Hi + 1
∀Pj ∈ Vi , Pi envoie REL(Hi ) à Pj
F _Hi [i] ← Hi
F _Mi [i] ← REL
end
Règle 5 : Pi reçoit REL(H) de Pj
begin
Hi ← max(Hi , H) + 1
F _Hi [j] ← H
F _Mi [j] ← REL
end

141
Synchronisation entre processus dans les systèmes distribués (BH)

Propriétés de l’algorithme de Lamport


— Sûreté : l’exclusion mutuelle est garantie. Nous pouvons en donner la preuve
par contradiction suivante :
— Supposons que Pi et Pj sont en même temps dans la SC à l’instant t,
— Alors la condition d’accès doit être valide sur les deux processus en même
temps et ils sont tous les deux dans l’état REQ
— On peut supposer qu’à un instant t, H(Si ) < H(Sj ) (ou l’inverse, ce qui
revient au même) puisque deux horloges logiques ne sont jamais identiques
— A partir de l’hypothèse de communication FIFO, il est clair que le message
de requête de Si est dans le tableau F _Mj au moment où Pj entre en SC
puisque Pj attend toutes les réponses (ACK) des processus avant d’entrer,
donc celui de de Pi .
— Or l’acquittement de Pi a été généré après que Pi ait émis le message REQ
puisque H(Si ) < H(Sj )
— Donc Pj est entré en SC alors que H(Si ) < H(Sj ) → contradiction
— Vivacité : toute demande d’entrée en Section Critique sera satisfaite au bout
d’un temps fini. En effet, après la demande d’un processus Pi , au plus (n-1)
processus peuvent entrer avant lui.
— L’algorithme est exempt d’interblocage.

Complexité de l’algorithme de Lamport Cet algorithme requiert 3 × (N − 1) mes-


sages par entrée en Section Critique, où N est le nombre de processus : (N − 1) re-
quêtes, (N − 1) acquittements, (N − 1) libérations.

Exercice 2 : Déroulement de l’algorithme de Lamport


Nous vous proposons dans cet exercice de "dérouler" l’algorithme de Lamport avec 3
processus P0 , P1 , P2 .
Les processus P2 et P1 demandent la Section Critique en 0.
Les horloges de chaque processus sont initialisées à 0.
Pour chaque processus Pi , on tient à jour :
— l’horloge locale
— un tableau de messages
— un tableau d’horloges
— les messages envoyés et reçus
Vous décidez de l’ordre de traitement des messages, en effet cet ordre peut varier
d’une exécution à l’autre. L’algorithme fonctionne quelque soit l’ordre, il faut juste
respecter le fait qu’un message doit être émis avant d’être reçu.
Vous arrêtez de dérouler l’algorithme lorsque les deux processus P2 et P1 seront
entrés et sortis de Section Critique.

Correction 5.7

142
Synchronisation entre processus dans les systèmes distribués (BH)

5.4.2 Algorithme d’exclusion mutuelle de Ricart et Agrawala

Cet algorithme a été proposé dans le but de diminuer le nombre de messages par
rapport à l’algorithme de Lamport.
L’algorithme repose sur les trois règles suivantes :
1. Lorsqu’un processus Pi demande la Section Critique, il diffuse une requête da-
tée à tous les autres processus.
2. Lorsqu’un processus Pi reçoit une requête de demande d’entrée en Section Cri-
tique de Pj , deux cas sont possibles :
— Cas 1 : si le processus Pi n’est pas demandeur de la Section Critique, il en-
voie un accord à Pj .
— Cas 2 : si le processus Pi est demandeur de la Section Critique et si la date
de demande de Pj est plus récente que la sienne, alors la requête de Pj est
différée, sinon un message d’accord est envoyé à Pj .
3. Lorsqu’un processus Pi sort de la Section Critique, il diffuse à tous les processus
dont les requêtes sont différées, un message de libération.

Algorithme d’exclusion mutuelle de Ricard et Agrawala (algorithme 44) Comme


pour l’algorithme précédent, nous utilisons la notion de règles correspondant aux évé-
nements locaux ou distants.

143
Synchronisation entre processus dans les systèmes distribués (BH)

Algorithme 44 : Algorithme d’exclusion mutuelle de Ricart et Agrawala


Variables utilisées par chaque processus Pi :
Hi : entier // estampille locale
HSCi : entier // cestampille de demande d’entrée en SC
Ri : booléen // le processus est demandeur de SC
Xi : ens. de processus // processus pour envoi de REL différé
N reli : entier // nombre de REL attendus
Vi : ens. de processus // voisinage de Pi
Messages utilisés :
REQ(H) : demande d’entrée en SC
REL() : message de permission/libération de SC
Initialisation des variables :
begin
Hi ← 0
HSCi ← 0
Ri ← F AU X
Xi ← ∅
N reli ← 0
end
Règle 1 : Pi demande la Section Critique
begin
Ri ← V RAI
Hi ← Hi + 1
HSCi ← Hi
N reli ← cardinal(Vi )
∀Pj ∈ Vi , Pi envoie REQ(HSCi ) à Pj
Attendre (N reli = 0)
< SC>
end
Règle 2 :Pi reçoit REQ(H) de Pj
begin
Hi ← max(Hi , H) + 1
if Ri ∧ ((HSCi < H) ∨ ((HSCi = H) ∧ i < j)) then
Xi ← Xi ∪ j
else
Pi envoie REL() à Pj
end if
end
Règle 3 : Pi reçoit REL() de Pj
begin
N reli ← N reli − 1
end
Règle 4 : Pi libère la Section Critique
begin
Ri ← F AU X
∀Pj ∈ Xi , Pi envoie REL() à Pj
Xi ← ∅
end

144
Synchronisation entre processus dans les systèmes distribués (BH)

Complexité de l’algorithme de Ricard et Agrawala :


Une utilisation de la Section Critique nécessite 2*(N-1) messages : (N-1) requêtes
et autant de renvois de permissions.

Exercice 3 : Algorithme de Ricart et Agrawala


Pourquoi chaque processus gère deux horloges logiques ?

Exercice 4 : Déroulement de l’algorithme de Ricart et Agrawala


Nous vous proposons de dérouler cet algorithme sur la même base que l’algorithme
de Lamport.
L’application est composée de 3 processus P0 , P1 , P2 .
Les horloges de chaque processus sont initialisées à 0.
Les processus P2 et P1 demande la Section Critique en 0.

Correction 5.7

5.4.3 Algorithme d’exclusion mutuelle de Carvalho et Roucairol

Il peut être vu comme une amélioration de l’algorithme de Ricart et Agrawala en


terme de nombre de messages.
Considérons le cas suivant : deux processus Pi et Pj , tels que Pi veut utiliser la
Section Critique plusieurs fois de suite alors que Pj n’est pas intéressé par celle-ci et
reste dans l’état dehors.
Avec l’algorithme de Ricart et Agrawala, Pi demande la permission de Pj à chaque
nouvelle invocation de l’opération acquérir. Le principe utilisé ici est le suivant :
puisque Pj a donné sa permission à Pi , ce dernier la considère comme acquise jusqu’à
ce que Pj demande sa permission à Pi . Avec cet algorithme, Pi ne demande qu’une
fois la permission à Pj .

145
Synchronisation entre processus dans les systèmes distribués (BH)

Algorithme 45 : Algorithme de Carvalho et Roucairol


Variables utilisées par chaque processus Pi :
Hi : entier // estampille locale
HSCi : entier // estampille de demande d’entrée en SC
Ri : booléen // le processus est demandeur de SC
SCi : booléen // le processus est en SC
Xi : ens. de processus // processus avec avis de libération différé
XAi : ens. de processus // processus desquels attend une autorisation
N reli : entier // nombre d’avis de libération attendus
Vi : ensemble de processus, // voisinage de Pi
Initialisation des variables :
Hi ← 0 ; HSCi ← 0 ; Ri ← F AU X ; SCi ← F AU X
Xi ← ∅ ; XAi ← Vi ; N reli ← 0
Messages utilisés :
REQ(H) : demande d’entrée en SC
REL() : message de permission

Règle 1 : Pi demande la SC
begin
Ri ← V RAI
Hi ← Hi + 1
HSCi ← Hi
N reli ← cardinal(XAi )
∀Pj ∈ XAi , Pi envoie REQ(HSCi ) à Pj
Attendre (N reli = 0)
SCi ← V RAI
< SC>
end
Règle 2 : Pi reçoit REQ(H) de Pj
begin
Hi ← max(Hi , H) + 1
if SCi ∨ (Ri ∧ ((HSCi < H) ∨ ((HSCi = H) ∧ i < j))) then
Xi ← Xi ∪ j
else
Pi envoie REL() à Pj
if Ri ∧ (¬SCi ) ∧ j ∈
/ XAi then
Pi envoie REQ(HSCi ) à Pj
N reli ← N reli + 1
end if
XAi ← XAi ∪ j
end if
end
Règle 3 : Pi reçoit REL() de Pj
begin
N reli ← N reli − 1
end
Règle 4 : Pi libère la SC
begin
Ri ← F AU X
SCi ← F AU X
XAi ← Xi
∀Pj ∈ Xi , Pi envoie REL() à Pj
Xi ← ∅
end

146
Synchronisation entre processus dans les systèmes distribués (BH)

Complexité de l’algorithme de Carvalho et Roucairol L’application de ce principe


permet de réduire le nombre de messages nécessaires pour une utilisation de la Section
Critique. Ce nombre est :
— pair : à toute requête d’entrée en SC correspond un envois de permission
— fonction de la structure du système : il varie entre 0 et 2*(n-1).

5.5 Synchronisation distribuée utilisant un jeton

L’exclusion mutuelle peut être réalisée à l’aide d’un jeton. Ce jeton, unique pour
l’application, représente la permission d’entrer en Section Critique. En effet, à un mo-
ment donné, un seul processus détient le jeton.
Plusieurs algorithmes utilisent cette notion. Par contre, ils ne reposent pas sur les
même structures de diffusion :
— structure en anneau : algorithme de Le Lann
— diffusion : algorithme de Suzuki-Kazami
— structure en arbre : algorithme de Naimi-Trehel (non présenté dans le cours)

5.5.1 Algorithme d’exclusion mutuelle de Le Lann

Construction de l’anneau Pour cet algorithme, un anneau virtuel est construit


avec l’ensemble des participants (processus) de l’application de façon logicielle. La
construction de l’anneau amène à attribuer une place à chacun des processus, chaque
processus connaissant son successeur. À l’initialisation, le jeton est attribué a un pro-
cessus (par exemple le processus P 0 ).

Principe de l’algorithme d’exclusion mutuelle de Le Lann


— Quand un processus reçoit le jeton de son prédécesseur dans l’anneau :
— s’il est demandeur de Section Critique, il garde le jeton et entre en Section
Critique
— s’il n’est pas demandeur de Section Critique, il envoie le jeton à son succes-
seur dans l’anneau
— A la sortie de Section Critique, le processus envoie le jeton à son successeur
dans l’anneau : un processus ne peut pas entrer deux fois de suite en Section
Critique

Commentaires :
— cet algorithme est facile à mettre en œuvre.
— cet algorithme nécessite des échanges de messages même si aucun site ne veut
accéder la Section Critique.
— le temps d’accès à la Section Critique peut être long

147
Synchronisation entre processus dans les systèmes distribués (BH)

— si le jeton se perd, il doit être régénéré. Il est difficile de détecter un tel cas.
On ne peut pas considérer le temps écoulé entre deux passages du jeton, car
le temps passé en Section Critique par les différents processus peut être très
variable. Des algorithmes gèrent ce problème de perte et régénération de jeton
sur un anneau, nous ne les étudions pas dans le cadre de ce cours.
— il y a également le problème de la panne d’un processus. Celui-ci est plus fa-
cile à gérer que dans les algorithmes précédents. En effet, si chaque processus
envoie un acquittement quand il reçoit le jeton, il est facile de détecter la panne
de l’un d’entre eux. Le processus mort peut être retiré de l’anneau et le sui-
vant prend alors sa place. Par contre, pour implanter ceci, il faut que chaque
processus maintienne la configuration courante de l’anneau.

5.5.2 Algorithme d’exclusion mutuelle de Suzuki et Kasami

Dans cet algorithme, les processus ne sont pas organisés en anneau.


L’accès à la Section Critique est matérialisé par un objet abstrait appelé jeton. Une
condition nécessaire et suffisante pour qu’un processus entre en section critique est
qu’il possède le jeton.
Le jeton est demandé par le processus Pi à l’aide d’un message de requête estam-
pillé et diffusé à tous les autres processus (Pi ne sait pas qui possède le jeton). Dans
le jeton est mémorisée l’estampille de la dernière visite qu’il a effectuée à chacun des
processus. Lorsque le processus Pj , qui possède le jeton, ne désire plus l’utiliser pour
accéder à la section critique, il cherche le premier processus Pk tel que l’estampille de
la dernière requête de Pk soit supérieure à l’estampille mémorisée par le jeton lors de
sa dernière visite à Pk .

148
Synchronisation entre processus dans les systèmes distribués (BH)

Algorithme 46 : Algorithme d’exclusion mutuelle de Suzuki et Kasami


Variables utilisées par chaque processus Pi :
Hi : entier // estampille
AJi : booléen // le processus a le jeton
SCi : booléen // le processus est en SC
F Hi [] : tableau // horloges des dernières requêtes des processus
Ji [] : tableau // mémorise le jeton
Vi : ensemble de processus // voisinage de Pi
Messages utilisés :
REQ[H] : requête d’entrée en Section Critique
M SGJET ON [J] : message permettant de transmettre le tableau d’horloges J
Règle 1 : Initialisation des variables
begin
if Pi = P0 then AJi ← V RAI
sinon AJi ← F ALSE
Hi ← 0 ; SCi ← F AU X ; F Hi [] ← 0 ; Ji [] ← 0
end
Règle 2 : Pi demande la Section Critique
begin
Hi ← Hi + 1
F Hi [i] ← Hi
if AJi = FAUX then
∀Pj ∈ Vi , Pi envoie REQ(Hi ) à Pj
Attendre (AJi = V RAI)
end if
SCi ← V RAI
<SC>
end
Règle 3 : Pi reçoit REQ(H) de Pj
begin
F Hi [j] ← H
if ((AJi = V RAI) ∧ (SCi = F AU X)) then
AJi ← F AU X
Pi envoie M SGJET ON (Ji ) à Pj
end if
end
Règle 4 : Pi reçoit M SGJET ON (J) de Pj
begin
Ji ← J
AJi ← V RAI
end
Règle 5 : Pi sort de Section Critique
begin
SCi ← F AU X
Ji [i] ← Hi
Trouver le premier processus Pk tel que F Hi [k] > Ji [k]
if Pk existe then
AJi ← F AU X
Pi envoie M SGJET ON (Ji ) à Pk
end if
end

149
Synchronisation entre processus dans les systèmes distribués (BH)

Commentaires sur l’algorithme de Suzuki et Kasami Chaque processus mémorise


les demandes d’entrée en section critique et l’estampille correspondante de chacun
des autres processus. Il y a un seul jeton pour l’application.
Il n’est pas nécessaire de synchroniser entre elles les horloges des différents proces-
sus car on ne les compare pas. La comparaison s’effectue uniquement entre horloges
issues d’un même processus.
Règle 1 : Initialisation : le jeton est attribué au processus P0 .
Règle 2 : Lorsque le processus Pi veut accéder à la section critique, il incrémente
son horloge logique. Il sauvegarde cette horloge dans le tableau des horloges
correspondant aux demandes de section critique. S’il ne possède pas le jeton,
il envoie une requête de demande de SC à ses voisins et se met en attente du
jeton. Sinon, s’il possède le jeton il entre en SC.
Règle 3 : Lorsque le processus Pi reçoit une requête de demande de SC du proces-
sus Pk , il enregistre, l’horloge reçue. S’il possède le jeton et qu’il n’est pas en SC
alors il met AJi à Faux et il envoie le jeton à Pk . Il faut positionner AJi à FAUX
avant d’envoyer le jeton. Sinon, si une requête est reçue entre l’envoi du jeton
et la mise à jour de AJi , on pourrait tenter d’envoyer une seconde fois le jeton.
Il serait même préférable de protéger cette partie de code par un mutex.
Règle 4 : Lorsque le processus Pi reçoit le jeton, il enregistre le fait qu’il entre en
SC et qu’il possède le jeton avant d’entrer en SC. On commence par positionner
le booléen SC à VRAI. Dans ce cas si un message REQ depuis k est reçu entre
l’exécution de cette règle et l’entrée en Section Critique, le jeton ne sera pas
envoyé à k.
Règle 5 : A la sortie de la SC, le processus Pi enregistre qu’il a quitté la SC et stocke
l’horloge à laquelle il a été en SC dans le jeton. Ensuite, il cherche si un proces-
sus a fait une requête d’entrée en SC qui n’a pas été satisfaite. Il envoie le jeton
au premier processus trouvé. Si aucun processus n’a demandé la SC, le proces-
sus conserve le jeton.

5.6 Bibliographie

Pour la rédaction de ce chapitre je me suis principalement inspirée des ouvrages


de A. Tanenbaum, M. Raynal et des cours de [Link] du Cnam.

5.7 Correction des Exercices

Correction Exercice 1 : Relations de précédence


E1.1 → E1.2 → E1.3 → E1.4 → E1.5 → E1.6 → E1.7

150
Synchronisation entre processus dans les systèmes distribués (BH)

E2.1 → E2.2 → E2.3 → E2.4 → E2.5 → E2.6 → E2.7 → E2.8 → E2.9 → E2.10
E3.1 → E3.2 → E3.3 → E3.4 → E3.5 → E3.6 → E3.7 → E3.8
E1.1 → E2.2
E1.4 → E2.9
E1.5 → E3.6
E2.1 → E3.1
E2.3 → E1.3
E2.4 → E3.2
E2,10 → E1,7
E3.3 → E2.7
E3.4 → E2.6
E3.7 → E2.8

Correction Exercice 2 : Déroulement de l’algorithme de Lamport

Dans le tableau ci-après, nous répertorions les évènements traités : messages en-
voyés et reçus. Nous ne connaissons pas l’ordre exact dans lequel ces messages sont
reçus, nous le choisissons arbitrairement. L’étoile dans la troisième colonne indique
que l’évènement a été traité. Nous donnons pour chaque processus participant (un ta-
bleau par processus) les modifications apportées à ses structures de données au cours
du déroulement de l’algorithme. Nous y reportons les numéros d’évènement du ta-
bleau précédent.

151
Synchronisation entre processus dans les systèmes distribués (BH)

Algorithme de Lamport
Numéro Evènements
1 P2 demande la Section Critique
1 P2 envoie REQ(1) à P0 *
1 P2 envoie REQ(1) à P1 *
1 P2 en Attente de Section Critique *
2 P1 demande la Section Critique
2 P1 envoie REQ(1) à P0 *
2 P1 envoie REQ(1) à P2 *
2 P1 en Attente de Section Critique *
3 P0 reçoit REQ(1) de P2
3 P0 envoie ACK(2) à P2 *
4 P1 reçoit REQ(1) de P2
4 P1 envoie ACK(2) à P2 *
5 P0 reçoit REQ(1) de P1
5 P0 envoie ACK(3) à P1 *
6 P2 reçoit REQ(1) de P1
6 P2 envoie ACK(2) à P1 *
7 P2 reçoit ACK(2) de P0
8 P2 reçoit ACK(2) de P1
9 P1 reçoit ACK(3) de P0
9 P1 entre en Section Critique
10 P1 reçoit ACK(2) de P2
11 P1 sort de Section Critique
11 P1 envoie REL(6) à P0 *
11 P1 envoie REL(6) à P2 *
12 P0 reçoit REL(6) de P1
13 P2 reçoit REL(6) de P1
13 P2 entre en Section Critique

Variables du processus P0
Evnt Init 3 5 12
H 0 2 3 7
F H[] [0,0,0] [0,0,1] [0,1,1] [0,6,1]
F M [] [„] [„REQ] [,REQ,REQ] [,REL,REQ]

Variables du processus P1
Evne Init 2 4 9 10 11
H 0 1 2 4 5 6
F H[] [0,0,0] [0,1,0] [0,1,1] [3,1,1] [3,1,1] [3,6,1]
F M [] [„] [,REQ,] [,REQ,REQ] [ACK,REQ,REQ] [ACK,REQ,REQ] [ACK,REL,REQ]

Variables du processus P2
Evnt Init 1 6 7 8 13
H 0 1 2 3 4 7
F H[] [0,0,0] [0,0,1] [0,1,1] [2,1,1] [2,1,1] [2,6,1]
F M [] [„] [„REQ] [,REQ,RQ] [ACK,REQ,REQ] [ACK,REQ,REQ] [ACK,REL,REQ]

152
Synchronisation entre processus dans les systèmes distribués (BH)

Correction Exercice 3 : Algorithme de Ricart et Agrawala


Pourquoi chaque processus gère deux horloges logiques ?
HSCi permet de conserver l’horloge valide lors de la dernière demande de SC. Hi est
mise à jour quand on reçoit, elle permet de gérer la synchronisation avec les autres
participants : c’est la plus grande horloge que l’on a vu passer.

Correction Exercice 4 : Déroulement de l’algorithme de Ricart et Agrawala

Dans le tableau ci-après, nous répertorions les évènements traités : messages en-
voyés et reçus. Nous ne connaissons pas l’ordre exact dans lequel ces messages sont
reçus, nous le choisissons arbitrairement. Nous donnons pour chaque processus parti-
cipant (un tableau par processus) les modifications apportées à ses structures de don-
nées au cours du déroulement de l’algorithme. Nous y reportons les numéros d’évè-
nements du tableau précédent.

153
Synchronisation entre processus dans les systèmes distribués (BH)

Algorithmle de Ricart et Agrawala


Numéro Evènements
1 P2 demande la Section Critique
1 P2 envoie REQ(1) à P0 *
1 P2 envoie REQ(1) à P1 *
1 P2 en attente de N rel2 == 0 *
2 P1 demande la Section Critique
2 P1 envoie REQ(1) à P0 *
2 P1 envoie REQ(1) à P2 *
2 P1 en attente de N rel1 == 0 *
3 P0 reçoit REQ(1) de P2
3 P0 envoie REL() à P2 *
4 P1 reçoit REQ(1) de P2
5 P0 reçoit REQ(1) de P1
5 P0 envoie REL() à P1 *
6 P2 reçoit REQ(1) de P1
6 P2 envoie REL() à P1 *
7 P2 reçoit REL() de P0
8 P1 reçoit REL() de P0
9 P1 reçoit REL() de P2
9 P1 entre en Section Critique
10 P1 sort de Section Critique
10 P1 envoie REL() à P2 *
11 P2 reçoit REL() de P1
11 P2 entre en Section Critique
12 P2 sort de Section Critique

Variables du Processus P0 Variables du Processus P1


Evnt Init 3 5 Evnt Init 2 4 8 9 10
HSC 0 0 0 HSC 0 1 1 1 1 1
H 0 2 3 H 0 1 2 2 2 2
R F F F R F V V V V F
X ∅ ∅ ∅ X ∅ ∅ {2} {2} {2} ∅
N Rel 0 N Rel 0 2 2 1 0 0

Variables du Processus P2
Evnt Init 1 6 7 11 12
HSC 0 1 1 1 1 1
H 0 1 2 2 2 2
R F V V V V F
X ∅ ∅ ∅ ∅ ∅ ∅
N Rel 0 2 2 1 0 0

154
Chapitre 6

Élection dans les systèmes


distribués (LP)

Contenu
6.1 Algorithme d’élection sur un arbre . . . . . . . . . . . . . . . . . . . 156
6.2 Algorithmes d’élection sur un anneau . . . . . . . . . . . . . . . . . 158
6.2.1 Algorithme d’élection de Le Lann : . . . . . . . . . . . . . . . . 158
6.2.2 Algorithme d’élection de Chang et Roberts pour anneau . . . 159
6.3 Algorithme du plus fort ou Bully algorithm de Garcia-Molina . . . . 159
6.4 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
6.5 Correction des Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . 161

En algorithmique distribuée, nous pouvons parfois avoir recours à un coordina-


teur ou initiateur, processus qui a un rôle particulier dans l’application. Par exemple,
l’algorithme de synchronisation centralisée présenté au chapitre précédent utilise un
coordinateur. Les algorithmes d’élection permettent de choisir ce coordinateur. Sou-
vent, il s’agit du processus de plus grand numéro.
A l’initialisation de l’application, le processus de plus grand numéro est désigné
comme étant le coordinateur, pour des raisons de simplicité. Par contre, si au cours
de l’application ce coordinateur quitte le groupe ou tombe en panne, il faut le rempla-
cer. Pour cela, nous utilisons un algorithme d’élection. Cet algorithme a pour but de
choisir un nouveau coordinateur.
Un algorithme d’élection est initié par un processus qui s’aperçoit que le coordi-
nateur n’est plus actif. Comme nous sommes en distribué, il peut y avoir plusieurs
processus qui s’aperçoivent, indépendamment les uns des autres, que le coordinateur
n’est plus actif. Ces processus sont appelés les initiateurs car ce sont eux qui initie
l’élection, donc qui lancent l’exécution de l’algorithme.
Suivant les algorithmes, les processus non-initiateurs, ceux qui ont reçu un mes-
sage d’élection avant de lancer eux-mêmes l’algorithme, ont un comportement entiè-
rement passif ou non. Lorsqu’un processus est passif il se contente généralement de
relayer les messages liés à l’algorithme d’élection.

155
Élection dans les systèmes distribués (LP)

Comme à l’initialisation c’est généralement le processus de plus grand numéro


qui est choisi. À noter tout de même que ce choix repose généralement que sur une
condition qui pourrait facilement être changée, par exemple pour élire le processus de
plus petit numéro, et que le choix pourrait reposer également reposer sur une valeur
possédée par le processus, à condition que cette dernière soit différente pour chacun
des processus.
Le choix du nouveau coordinateur peut alors être réalisé uniquement parmi les
initiateurs ou sur l’ensemble des processus. Le fait de ne choisir le nouveau coordi-
nateur que parmi l’ensemble des initiateurs peut permettre de limiter le nombre de
messages échangés.
Pour la suite, nous posons les hypothèses suivantes :
— que chacun des processus a au moins un numéro unique, par exemple son
adresse réseau (Attention ! cela implique un seul processus par machine,
comme nous l’avons déjà vu).
— que tous les processus connaissent le numéro de chacun des autres, mais un
processus ne sait pas qui est actif et qui ne l’est pas.
— que les réponses aux messages sont reçues au bout d’un temps borné par
TEMPO.
— aucun processus ne tombe en panne pendant le déroulement de l’algorithme
d’élection 1

6.1 Algorithme d’élection sur un arbre

Dans cet algorithme, les processus sont structurés en arbre. Le principe général de
l’algorithme est le suivant :
— Un (ou plusieurs) processus détecte le départ du coordinateur
— Le processus qui a détecté l’absence du coordinateur informe l’ensemble des
processus du début de l’algorithme avec un message < wakeup >. Cette phase
de réveil permet que tous les processus soient parcourus et donc qu’ils exé-
cutent la partie élection de l’algorithme.
— Lorsque le message < wakeup > a parcouru tout l’arbre l’élection commence.
— La seconde partie de l’algorithme est initiée par toutes les feuilles de l’arbre.
En effet, après avoir passées la phase de réveil, seules les feuilles n’entrent pas
dans le premier tant que de la partie l’élection de l’algorithme, car elles n’ont
qu’un voisin (leur père).
— le processus ayant le plus grand numéro est élu.
L’algorithme d’élection sur un arbre est donné dans l’algorithme 47. Il est donné
sous la forme d’un seul algorithme qui s’exécute pour tous les processus, ce qui vous
1. Nous avons conservé cette hypothèse pour limiter la complexité des algorithmes, même si elle peut
être peu réaliste dans certains contextes.

156
Élection dans les systèmes distribués (LP)

permet de voir un autre formalisme possible pour les algorithmes distribués. ce forma-
lisme est, par exemple, utilisé dans le livre “Introduction to Distributed Algorithms”
de Gerard Tel.
Algorithme 47 : Algorithme d’élection sur un arbre
Variables pour chaque processus Pi :
wsi : booléen init F AU X // wsi est vrai si Pi est réveillé
wri : entier init 0 // nombre de messages de reveil reçus
reqi [j], ∀Pj ∈ N eighi : booléen init F AU X // vrai si a reçu un msg de Pj
vi : numéro de processus init Pi // plus grand processus
statei : (sleep, leader, lost) init sleep
N eighi : ensemble des voisins du processus
Messages :
WAKEUP : annonce de réveil avant une élection
TOKEN(r) : message d’élection
Partie réveil :
begin
if Pi est initiateur then
wsi ← V RAI
for Pj ∈ N eighi do Pi envoie WAKEUP à Pj
end if
while wri < ♯N eighi do
Pi reçoit WAKEUP de Pj
wri ← wri + 1
if wsi = F AU X then
wsi ← V RAI
for Pj ∈ N eighi do Pi envoie WAKEUP à Pj
end if
end while
end
Partie élection :
begin
while ♯(j : reqi [j] = F AU X) > 1 do
Pi reçoit TOKEN(r) de Pj
reqi [j] ← V RAI
vi ← max(vi , r)
end while
Pi envoie TOKEN(vi ) à Pk tel que reqi [k] = F AU X
Pi reçoit TOKEN(r) de Pk
vi ← max(vi , r)
if vi = Pi then statei ← leader else statei ← lost
for Pj ∈ N eighi , j ̸= k do Pi envoie TOKEN(vi ) à Pj
end

157
Élection dans les systèmes distribués (LP)

6.2 Algorithmes d’élection sur un anneau

Pour les deux algorithmes suivants, les processus sont organisés en anneau.

6.2.1 Algorithme d’élection de Le Lann :

Principe de l’algorithme
1. P lance l’élection en envoyant un message ELECTION contenant son numéro
de processus à son successeur ou au premier successeur actif.
2. À la réception d’un message ELECTION, le processus P vérifie si son numéro
est en première position des numéros stockés :
— dans ce cas, le message ELECTION a fait un tour, le coordinateur est le
processus ayant le plus grand numéro stocké dans le message. Le processus
P, transforme le message en COORDINATEUR et le fait circuler sur l’anneau
pour informer les participants du nouveau coordinateur.
— dans le cas contraire, le processus P ajoute son numéro au message et il le
transmet au prochain successeur actif.
3. À la réception d’un message COORDINATEUR, le processus P recherche le
plus grand processus de la liste :
— s’il correspond à son numéro, alors il est le nouveau coordinateur
— il enregistre ce numéro
S’il est le premier de la liste, il détruit le message, sinon il le transmet tel quel à
son successeur.

Exercice 1 : Écriture de l’algorithme d’élection de Le Lann


Nous vous proposons d’écrire l’algorithme d’élection de Le Lann.

Correction 6.5

Déroulement de l’algorithme d’élection de Le Lann : Prenons pour exemple une


application composée de 8 processus, numérotés de 0 à 7. Le processus 7 est initiale-
ment le coordinateur. Il quitte le groupe, les processus 2 et 5 s’en aperçoivent et lancent
une élection. Regarder ce qui se passe avec deux messages en transit sur l’anneau.

Complexité de l’algorithme d’élection de Le Lann


— Nombre de messages :O(N 2 ) (N initiateurs * N processus dans l’anneau)
— Temps : 2N − 1

158
Élection dans les systèmes distribués (LP)

6.2.2 Algorithme d’élection de Chang et Roberts pour anneau

Principe Cet algorithme est une évolution de l’algorithme de Le Lann . Si plusieurs


élections sont lancées en même temps, il vise à supprimer les jetons des processus qui
ne sont pas élus c’est à dire ceux qui ont un numéro de processus plus petit.

6.3 Algorithme du plus fort ou Bully algorithm de Garcia-


Molina

Principe de l’algorithme : Quand un processus P s’aperçoit que le coordinateur ne


répond plus à ses requêtes, il lance l’algorithme d’élection.

1. Le processus P lance une élection en envoyant un message ELECTION à tous


les autres processus dont le numéro est plus grand que le sien :
— Si aucun processus ne lui répond, P gagne l’élection et devient le coordina-
teur.
— Si un processus de numéro plus élevé répond, c’est lui qui prend le pouvoir.
Le rôle de P est terminé.
2. À la réception d’un message ELECTION, un processus Q envoie un message
ACK à l’émetteur lui signifiant qu’il est actif. À son tour Q, lance une élection
si ce n’est pas déjà fait.
3. Le nouveau coordinateur envoie un message à tous les participants pour les
informer de son rôle. L’application peut alors continuer à s’exécuter.

Si un processus qui était inactif se réveille, il déclenche une élection. S’il détient
le plus grand numéro de processus en cours de fonctionnement, il gagne l’élection et
devient le nouveau coordinateur.

Exercice 2 : Écriture de l’algorithme du plus fort


Écrire l’algorithme correspondant sous forme de règles.
Correction 6.5

Exercice 3 : Déroulement de l’algorithme du plus fort

Prenons pour exemple une application composée de 8 processus, numérotés de 0


à 7. Le processus 7 tombe quitte le groupe, le processus 4 est le premier à le remar-
quer. Décrivez le déroulement de l’élection d’un nouveau coordinateur. Est ce que le
processus 7 est obligé de lancer une élection quand il reprend son activité ?

Correction 6.5

159
Élection dans les systèmes distribués (LP)

6.4 Bibliographie

Pour la rédaction de ce chapitre je me suis principalement inspiré des ouvrages


suivants :
— “Distributed Systems, Principles and Paradigms” de A. Tanenbaum et S. Van
Steen, [Link]
— “Introduction to Distributed Algorithms”, G. Tel, Camdbridge University Press

160
Élection dans les systèmes distribués (LP)

6.5 Correction des Exercices

Correction Exercice 1 : Écriture de l’algorithme d’élection de Le Lann

Algorithme 48 : Algorithme d’élection de Le Lann


Variables locales à chaque processus Pi
suivi , predi : entier // numéro du successeur/predecesseur
numcoi : entier // numéro du coordinateur
etati : ∈ {endormi, elu, perdu, initiateur}, init endormi // état du processus
f in_elei : booléen, init FAUX // Anneau parcouru par msg ELECT ION
f in_coi : booléen, init FAUX // Message COOR a parcouru l’anneau
listei : liste, init vide // liste processus traversés
Messages :
ELECT ION (visit) : message contenant la liste des processus qu’il a visité
COOR(actif ) : message contenant la liste des processus actifs
Règle 1 : déclenchement une élection
begin
etati ← initiateur
Listei .add(i)
Pi envoie ELECT ION (listei ) à suivi
Attendre (f in_elei = V RAI)
numcoi ← max(listei )
Pi envoie COOR(Listei ) à suivi
Attendre (f in_coori = V RAI)
end
Règle 2 : Pi reçoit ELECT ION (visit) de predi
begin
if etati = initiateur et i = [Link]() then
listei ← visit
f in_elei ← V RAI
else
[Link](i)
Pi envoie ELECT ION (visit) à suivi
end if
end
Règle 3 : Pi reçoit COOR(actif ) de predi
begin
if etati = initiateur et i = [Link]() then f in_coori ← V RAI
else
numcoi ← max(actif )
Pi envoie COOR(actif ) à suivi
end if
if numcoi = i then etati ← elu else etati ← perdu
end

161
Élection dans les systèmes distribués (LP)

Correction Exercice 2 : Écriture de l’algorithme du plus fort

Algorithme 49 : Algorithme du plus fort


Variables locales à chaque processus Pi :
numcoi : entier // numéro du coordinateur
elei : booléen, init FAUX // Indique s’il y a une élection en cours
oki : booléen, init FAUX // Indique la réception d’un acquittement
newcoori : booléen, init FAUX // Indique réception du coordinateur
tempoi : entier // temps maximal d’attente d’un acquittement
Vi : ensemble // ensemble des voisins
Messages
ELECT ION : message indiquant qu’une élection est lancée
ACK : acquittement à un message élection
COOR : message contenant le nouveau numéro du coordinateur
Règle 1 : déclenchement d’une élection
begin
elei ← V RAI
oki ← F AU X
newcoori ← F AU X
for Pj ∈ Vi tel que j > i do Pi envoie ELECT ION à Pj
Attendre (tempoi ou (oki = V RAI))
if oki = V RAI then
Attendre (newcoori = V RAI )
else
numcoi ← i
for j ∈ Vi do Pi envoie COOR à Pj
end if
elei ← F AU X
end
Règle 2 : Pi reçoit ELECT ION de Pj
begin
Pi envoie ACK à Pj
if elei = F AU X then Déclenchement d’une élection
end
Règle 3 : Pi reçoit ACK de Pj
begin
oki ← V RAI
end
Règle 4 : Pi reçoit COOR de Pj
begin
numcoi ← j
newcoori ← V RAI
end

Correction Exercice 3 : Déroulement de l’algorithme du plus fort

162
Élection dans les systèmes distribués (LP)

Le processus 4 est le premier à le remarquer, il envoie un message ELECTION à 5


et 6. En fait il envoie également un message au processus 7 mais dont il n’obtient pas
de réponse.
Les processus 5 et 6 répondent par OK à 4. Celui-ci sait que son rôle est terminé
dès réception du premier OK.
Les processus 5 et 6 organisent une élection, en envoyant un message d’élection
aux processus ayant un numéro plus fort que le leur. C’est le cas du processus 5 qui
envoie un message au processus 6 et au processus 7 et du processus 6 qui envoie un
message au processus 7. Lorsque le message du processus 5 arrive au processus 6,
celui-ci lui répond par un ACK ce qui lui indique qu’il doit quitter l’élection. Pour le
processus 6, après l’attente de T EM P O, il sait qu’il est le nouveau coordinateur. Le
processus l’annonce par un message COOR coordinateur à tous les autres processus
en exécution.
Quand 4 reçoit le message, il peut continuer l’opération qu’il était en train de faire
mais cette fois en utilisant 6 comme coordinateur.
Si le processus 7 repart, il lui suffit d’envoyer un message COORD à tous les autres
pour les soumettre à ses ordres. En effet, ce processus est sûr d’être le processus actif
de plus grand numéro.

163
Chapitre 7

Communication de groupe (LP)

Contenu
7.1 Les horloges vectorielles . . . . . . . . . . . . . . . . . . . . . . . . . 165
7.1.1 Rappel sur l’ordre causal . . . . . . . . . . . . . . . . . . . . . 166
7.1.2 Rappel sur les estampilles . . . . . . . . . . . . . . . . . . . . . 167
7.1.3 Les historiques . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
7.1.4 Projection de l’historique . . . . . . . . . . . . . . . . . . . . . 169
7.1.5 Les horloges vectorielles . . . . . . . . . . . . . . . . . . . . . . 170
7.2 La diffusion fiable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
7.3 Les ordres de délivrance des messages . . . . . . . . . . . . . . . . . 174
7.3.1 L’ordre FIFO (utilisé par FBCAST) . . . . . . . . . . . . . . . . 175
7.3.2 L’ordre causal (utilisé par CBCAST) . . . . . . . . . . . . . . . 175
7.3.3 L’ordre atomique ou total (utilisé par ABCAST) . . . . . . . . 176
7.4 Protocoles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
7.4.1 Le protocole FBCAST . . . . . . . . . . . . . . . . . . . . . . . 177
7.4.2 Le protocole CBCAST . . . . . . . . . . . . . . . . . . . . . . . 177
7.4.3 Le protocole ABCAST . . . . . . . . . . . . . . . . . . . . . . . 178
7.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
7.6 Bibliographie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

Ce chapitre aborde le problème de la communication de groupe, aussi appelée diffu-


sion fiable ou Reliable Multicast 1 .
La notion de groupe, telle que nous l’entendons dans ce chapitre, recouvre simple-
ment un ensemble de processus. Ces processus se structurent en groupe pour diffé-
rentes raisons comme par exemple celle de rendre un service à plusieurs ou celle de
permettre des échanges privés. Cette structure peut reposer sur les services offerts par
le systèmes, un middleware ou une couche applicative. La manière dont cette struc-
turation est établie n’est pas abordée dans le cours. Le calcul parallèle, les réseaux, le
travail collaboratif sont des exemples de contextes dans lesquels les groupes de pro-
cessus sont utilisés.
1. Au sens TCP du terme, un multicast est une communication globale, une diffusion, à l’intérieur
d’un groupe restreint, auquel on accède par une demande de participation ou subscribe.

164
Communication de groupe (LP)

A l’intérieur de ce groupe, pour réaliser les échanges directs entre deux processus,
ces derniers utilisent des procédures de communication classiques, telles que celles
que nous avons utilisées dans les chapitres précédents. Nous nous intéressons dans
ce chapitre à un type de communication particulier : l’envoi depuis un processus
vers l’ensemble du groupe, appelé diffusion. Il est important, pour la mise en place
d’un service ou pour synchroniser des applications, de s’interroger sur l’incidence de
l’ordre d’arrivée des messages dans ce type de communication et de pouvoir imposer
qu’un message envoyé à tous les membres d’un groupe arrive en respectant certaines
conditions sur l’ordre des réceptions. Nous parlons alors de diffusion fiable.
Comme dans le chapitre précédent, nous posons des hypothèses sur le système
distribué et ses propriétés. Nous considérons que chaque processus s’exécute sur un
processeur dédié 2 . De ce fait, nous parlons indifféremment de processus ou de proces-
seur pour désigner l’entité qui exécute l’algorithme et réalise les échanges de messages.
Nous parlons d’événements pour toutes les étapes qui ont de l’importance dans le pro-
cessus et nous faisons la différence entre les événements internes, les événements d’en-
voi ou les événements de réception des messages. Nous faisons l’hypothèse que les
messages ne sont jamais perdus et qu’ils arrivent au bout d’un temps fini. Par contre,
leur ordre d’arrivée n’est pas garanti. Les algorithmes de diffusion fiable peuvent éga-
lement garantir l’absence de duplication des messages. De même nous supposons que
les processus qui participent à la diffusion fiable ne tombent pas en panne. La notion
de fiabilité dont il est question ici s’applique à l’ordre de réception et ne concerne pas
la tolérance aux fautes.
A l’issue de ce chapitre, vous serez en mesure de :
— expliquer précisément les garanties qu’offre la diffusion fiable ;
— définir formellement et comparer plusieurs ordres de délivrance de messages ;
— déterminer l’ordre de délivrance respecté par un ensemble de communications ;
— analyser un algorithme de diffusion fiable pour établir les garanties fournies ;
— rédiger un algorithme de diffusion fiable respectant un protocole donné ;
— expliquer le lien entre les concepts d’horloges logiques et de diffusion fiable.

Nous présentons dans ce chapitre différents protocoles de diffusion fiable comme


la diffusion FIFO, causale ou atomique. Ces protocoles s’appuient sur les horloges
logiques pour ordonner les événements dans le groupe, celles-ci sont donc d’abord
détaillées avant de définir formellement le principe de diffusion fiable et de décrire
plusieurs protocoles de diffusion.

7.1 Les horloges vectorielles

Ordonner des événements dans un système est une fonctionnalité utile par
exemple pour la synchronisation, qu’elle soit centralisée ou distribuée. Nous avons vu
2. D’un point de vue conceptuel cela ne pose pas de problème par rapport aux architectures multi-
cœurs ou multi-processeurs puisque ces derniers peuvent également communiquer entre eux.

165
Communication de groupe (LP)

au chapitre précédent que les estampilles peuvent permettre de déterminer un ordre


dans un système distribué, en l’absence d’horloge globale. L’ordre donné par les es-
tampilles ne permet cependant pas de traduire la dépendance causale. Les horloges
vectorielles sont une solution à ce problème. Après un rappel sur l’ordre causal et les
estampilles, nous présentons la notion d’historique et les horloges vectorielles.

7.1.1 Rappel sur l’ordre causal

Les evénnements d’un système peuvent être ordonnés en fonction des dépen-
dances qu’ils ont entre eux. Une dépendance entre deux événements est définie dans
les cas suivants : deux événements internes d’un même processus, une émission et les
événements internes du processus émetteur, une réception et les événements internes
du processus récepteur et enfin l’émission et la réception d’un message. Un événement
précédent un autre événement a potentiellement une incidence sur le comportement
de ce dernier.
Parallélement à la notion de précédence nous pouvons définir la notion de causa-
lité qui définit un ordre entre des événements ayant une relation sémantique. C’est-
à-dire une relation qui tient compte de l’impact réel, et non plus potentiel, d’un évé-
nement sur un autre. On dit alors qu’un événement ei produit un événement ej si et
seulement si ej est une conséquence de ei . On note alors ei → ej . A noter que la re-
lation de causalité est équivalente à la relation de précédence dans le cas où chaque
événement d’un processus est considéré comme impactant ou produisant le suivant.
Les deux relations ont alors les mêmes propriétés et sont donc considérées indiffére-
ment dans la suite.
La dépendance causale entre deux événements ea et eb (ea → eb ) signifie que :
— soit ea précède eb sur le même processus (ordre séquentiel) ;
— ou ea correspond à l’envoi du message reçu en eb ;
— ou il existe un événement ec tel que ea → ec → eb .

L’ensemble des événements constituant la chaîne de dépendances entre un évé-


nement initial cause et un événement final conséquence est appelé chemin causal. Si
ea → eb , alors il existe au moins un chemin causal entre ea et eb . Lorsqu’il n’existe pas
de chemin reliant deux événements, on parle d’indépendance causale, notée (ea ∥ eb ).
La relation de dépendance causale est donc une relation irréflexive, anti-
symétrique et transitive. C’est donc une relation d’ordre stricte. L’ordre défini est ce-
pendant partiel car il n’est pas possible de définir une relation de causalité entre n’im-
porte quelle paire d’événements. Par exemple sur la figure 7.1, les événements e11 et
e21 n’ont pas de relation de dépendance et, d’une manière générale, des événements
qui s’exécutent en parallèle n’ont pas de dépendance causale, même si ils peuvent
avoir des dépendances communes.

166
Communication de groupe (LP)

7.1.2 Rappel sur les estampilles

Lamport a introduit la notion d’estampille afin d’ordonner, en fonction des dépen-


dances, les événements au sein d’un ensemble de processus distribués. Les estam-
pilles permettent de garantir que deux événements dépendants l’un de l’autre aient
des dates cohérentes. Ainsi, la date d’envoi d’un message ne peut pas être supérieure
à la date de réception.
Les estampilles sont des valeurs locales à chaque processus. Chaque processus i
dispose d’une estampille notée Hi . Elle est calculée en incrémentant la valeur associée
de 1 à chaque événement interne ou envoi et mise à max(Hi , Hp )+1 à la réception d’un
message du processus p. Si on ajoute le numéro de processus comme discriminant à
l’estamplille, celles-ci définissent un ordre total, c’est-à-dire que tous les événements
peuvent être ordonnés les uns par rapport aux autres.

1 2 3
P1
e11 e12 e13
<
1 2
P2
e21 e22
|| =
1 3 >
P3
e31 e32 dépendance estampilles
causale

F IGURE 7.1 – Relation entre la dépendance causale et les estampilles.

Tout en restant cohérentes avec la notion de dépendance causale, les estampilles


l’effacent. En effet il n’est pas possible, à partir de deux valeurs d’estampilles de savoir
si les événements estampillé ont une dépendance. Par exemple, sur la figure 7.1, la
valeur d’estampille 3 en e13 ne permet pas de connaître les chemins causals qui ont
e13 pour événement final. De même, si H(e1 ) < H(e2 ), on ne peut pas savoir s’il existe
un événement e tel que e1 précède e et/ou e précède e2 . Par exemple, sur la figure 7.1,
la valeur d’estampille de 3 en e32 et de 1 en e31 ne permet pas de savoir si il y a eu un
événement ayant une valeur d’estampille de 2 sur le processus 3.

Définition 7.1 Densité des estampilles


Un ensemble d’estampilles est dense si et seulement si pour toute paire d’estampilles H1 <
H2 , il existe un événement qui précède H2 et/ou qui suit H1 .

Les estampilles de Lamport ne sont pas denses, elles ne conservent pas l’informa-
tion sur la dépendance causale. La figure 7.1 illustre la relation entre les valeurs des
estampilles et la notion de dépendance liant les événements.

167
Communication de groupe (LP)

Pour certaines applications, comme le déboggage ou la communication de groupe,


il est cependant important de savoir si deux événements sont bien indépendants, on
parle alors d’indépendance causale, ce que ne permet pas les estampilles. La seule chose
que signifie H(ea ) < H(eb ) est que ea ne peut pas être la conséquence de eb : ¬(eb →
ea ). Il y a donc soit indépendance causale (ea ∥ eb ), soit une relation de précédence
(ea → eb ).

7.1.3 Les historiques

L’ensemble des dépendances causales d’un événement e est appelé son historique,
notée hist(e). L’historique de e est définie comme l’ensemble des événements apparte-
nant à tous les chemins causals conduisant à e, donc l’ensemble des événements dont
il dépend :

hist(e) = {e} ∪ {e′ | e′ → e}

e11 e12 e13


P1

e21 e22 e23 e24 e25


P2

e31 e32 e33 e34


P3

F IGURE 7.2 – Exemple d’exécution sur 3 processus communicants distribués.

La figure 7.2 illustre un exemple d’exécution distribuée sur 3 processus. Sur cet
exemple, l’historique de l’événement e33 (surligné en vert) est le suivant :

hist(e33 ) = {e11 , e21 , e22 , e23 , e24 , e25 , e31 , e32 , e33 }

Appartiennent donc à l’historique d’un événement tous les événements qui le pré-
cèdent sur le même processus plus tous les événements des autres processus ayant
une relation de dépendance avec l’un des événements, ayant lieu avant e33 de ce pro-
cessus, relation qui n’est établie que par une communication.
L’historique d’un événement e peut être utilisée pour la datation de e. Elle a l’avan-
tage, par rapport aux estampilles, de conserver l’information de la dépendance cau-
sale. L’examen de l’historique d’un événement permet en effet de retrouver ses dépen-
dances causales. Les propositions suivantes permettent de conclure quant à la dépen-
dance de deux événements e et e′ :

(
e → e′ ⇔ e ∈ hist(e′ ) ∧ e ̸= e′
e ∥ e′ ⇔ (e ∈
/ hist(e′ )) ∧ (e′ ∈
/ hist(e))

168
Communication de groupe (LP)

L’inconvénient de l’historique hist(e) d’un événement e est sa taille qui croît conti-
nuellement au cours du temps. Il n’est donc pas envisageable d’utiliser les historiques
pour réaliser la datation au sein d’un algorithme distribué.

7.1.4 Projection de l’historique

Pour résoudre le problème de la taille croissante de l’historique nous commençons


par définir la projection de l’historique de la manière suivante :

Définition 7.2 Projection de l’historique


La projection de l’historique hist(e) d’un événement e sur le processeur Pi est l’ensemble
histi (e) tel que :
histi (e) = {e′ | e′ ∈ hist(e) ∧ e′ a lieu sur Pi }

La projection de l’historique d’un événement sur un processeur est donc l’en-


semble des événement s’exécutant sur ce processeur et dont dépend notre événement.
Sur l’exemple d’exécution donné à la figure 7.2 on a :

hist(e33 ) = {e11 , e21 , e22 , e23 , e24 , e25 , e31 , e32 , e33 }

Les projections de hist(e33 ) sur les processeurs P1 , P2 et P3 sont respectivement :


— hist1 (e33 ) = {e11 }
— hist2 (e33 ) = {e21 , e22 , e23 , e24 , e25 }
— hist3 (e33 ) = {e31 , e32 , e33 }

L’union des projections constitue donc l’historique de l’événement. Nous pouvons


maintenant constater la propriété suivante :

Propriété 7.1 Si ei,k est un événement de projection de l’historique de e sur Pi , alors tout
événement antérieur à ei,k est dans l’historique de e :

∀(e, i, k), ei,k ∈ histi (e) ⇒ ∀j < k, ei,j ∈ hist(e)

Cette propriété permet de réduire la représentation de l’historique. En effet, en


considérant que l’événement ei,k est le plus récent des événements de l’historique de e
sur Pi , alors tous les événements ei,j , avec 0 < j < k, appartiennent à cet historique. Il
suffit donc de ne conserver que l’entier k pour caractériser la projection de l’historique
de e sur Pi . Ceci nous permet de donner une représentation réduite de l’historique
dont la taille ne croît pas au cours du temps.

Propriété 7.2 Représentation d’un historique par un vecteur

169
Communication de groupe (LP)

S
Comme hist(e) = i histi (e), un vecteur V (e), avec une valeur par processeur, suffit
pour représenter hist(e). Ce vecteur est défini de la manière suivante :

∀i ∈ [1, n], V (e)[i] = max{k : ei,k ∈ histi (e)}

V (e)[i] représente le nombre d’événements de Pi connus de e, c’est-à-dire connus sur le site de


e immédiatement après l’occurrence de e.

Dans l’exemple précédent on a :


— V (e33 )[1] = 1
— V (e33 )[2] = 5
— V (e33 )[3] = 3

7.1.5 Les horloges vectorielles

Les horloges vectorielles s’appuyent sur la projection de l’historique d’un événe-


ment pour dater ces derniers en conservant les relations de dépendance causale.

Définition 7.3 Les horloges vectorielles


On définit une horloge vectorielle par le vecteur V comme suit :
— on associe un vecteur Vi de taille n à chaque processus Pi , pour 1 ≤ i ≤ n.
— initialement Vi = (0, . . . , 0)
— à chaque événement local à Pi , Vi [i] ← Vi [i] + 1
— chaque message m porte l’estampille Vm (Vm est l’horloge vectorielle de l’émetteur)
— à la réception de (m, Vm ) par un processus Pi :
— Vi [i] ← Vi [i] + 1
— Vi [j] ← max(Vi [j], Vm [j]) pour 1 ≤ j ≤ n et j ̸= i

La figure 7.3 illustre un exemple d’exécution d’événements sur quatre processeurs.


Cet exemple montre des communications et des événements internes. Les événements
internes ou locaux, les envois et les réceptions de messages incrémentent la compo-
sante locale de l’horloge vectorielle. Lors d’une réception, une mise à jour des autres
composantes de l’horloge vectorielle locale est faite.

Propriété 7.3 Les horloges vectorielles


Soit deux horloges vectorielles V et V ′ . L’ordre partiel entre ces 2 horloges est régi par les
règles suivantes :

V ≤ V ′ si et seulement si ∀i, V [i] ≤ V ′ [i]


V < V ′ si et seulement si (V ≤ V ′ ) ∧ (V ̸= V ′ )
V ∥ V ′ si et seulement si ¬(V < V ′ ) ∧ ¬(V ′ < V )

170
Communication de groupe (LP)

(0,0,0,0) (1,1,0,0) (2,1,0,0) (3,1,0,0)


P1
evt local
1 3
(0,0,0,0) (0,2,0,0) (2,3,3,1)
P2
(0,1,0,0) evt local
4
(0,0,0,0) (0,0,1,1)
P3
(2,1,2,1) (2,1,3,1)
2
(0,0,0,0) evt local
P4
(0,0,0,1) (0,0,0,2)

F IGURE 7.3 – Exemple d’exécution d’une séquence d’événements sur 4 processeurs.

Propriété 7.4 Les horloges vectorielles sont denses


Soit ei un événement du processeur Pi et ej un événement du processeur Pj . Si V (ei )[k] <
V (ej )[k] avec k ̸= j, alors il existe un événement ek sur le processeur Pk tel que ¬(ek →
ei ) ∧ (ek → ej ).

Autrement dit, il existe forcément un événement ek qui a permis l’incrémentation


de la composante k de l’horloge sur le processeur Pk . Cet événement a eu lieu inévita-
blement avant l’événement ej sans qu’on puisse dire s’il s’est produit avant ou après
l’événement ei . La seule chose que l’on puisse affirmer, c’est que ek n’est pas la cause
de ei .

7.2 La diffusion fiable

La diffusion d’information au sein d’un groupe de processus est une fonctionna-


lité importante pour permettre d’informer l’ensemble des processus du groupe, par
exemple pour partager une information. Nous avons vu par exemple que certains
algorithmes de synchronisation distribuée réalisaient une diffusion pour faire leur de-
mande d’entrée en section critique auprès des processus participants. Cette diffusion
peut être réalisée simplement par le processus qui souhaite diffuser en envoyant suc-
cesssivement à chacun de ses voisins le même message ou de manière plus perfor-
mante avec des algorithmes de diffusion tels que ceux basés sur les arbres binômiaux.
Dans le cas de plusieurs diffusions concurrentes, et quelque soit le protocole de dif-
fusion utilisé, se pose le problème de l’ordre de réception des messages. La figure 7.4
illustre les problèmes qui peuvent survenir dans l’ordre de réception des messages.
Sur cette figure sont représentées trois diffusions : Dif f 1 réalisée par le processus
P 1, Dif f 2 réalisée par le processus P 3 et Dif f 3 réalisée par le processus P 1. Si nous
observons l’ordre de réception des messages sur les différents processus nous avons :
— Sur le processus P 1 : Dif f 1, Dif f 3 puis Dif f 2

171
Communication de groupe (LP)

P1 P2 P3
Diff1

Diff2

Diff3

temps

F IGURE 7.4 – Exemple d’exécution correcte d’une diffusion fiable de type FIFO.

— Sur le processus P 2 : Dif f 1, Dif f 2 puis Dif f 3


— Sur le processus P 3 : Dif f 3, Dif f 2 puis Dif f 1
Nous constatons ici que la dépendance de causalité n’est pas respectée puisque
sur le processus P 3 la diffusion Dif f 3 est reçue avant la réception Dif f 1 or la pre-
mière est potentiellement porteuse de dépenses par rapport à la dernière et elle est
reçue avant. De même sur l’ensemble des processus l’ordre de réception est différent.
Pour finir, la relation causalité entre la diffusion Dif f 3 et Dif f 2 imposée par la dé-
pendance entre les deux événements internes du processus P 3 n’est pas respectée sur
le processus P 2. Pour garantir l’ordre de dépendance temporelle, les relations de cau-
salité et l’ordre de réception des messages sur les différents processus, il est nécessaire
de mettre en place un algorithme de diffusion qui vérifie les priopriétés attendues,
c’est le rôle des algorithmes de diffusion fiable.
Nous n’avons aucun contrôle sur le temps de communication des messages, ni
sur la date d’arrivée ou de réception des messages. Par contre, rien ne n’empêche
un processus de différer la délivrance d’un message au processus destinataire afin de
garantir les propriétés attendues. Cela ne va pas dans le sens d’une plus grande per-
formance du système, mais plutôt dans le sens de la garantie du bon fonctionnement
d’une application distribuée. Les algorithmes de diffusion fiable s’appuyent ainsi sur
la distinction entre réception d’un message et délivrance de ce message. Une machine, la
couche de communication dans laquelle est implantée l’algorithme de diffusion fiable,
reçoit d’abord un message et peut le mettre en attente avant de le délivrer au processus
destinataire afin de respecter l’ordre qui doit être garanti par l’algorithme.

Définition 7.4 Diffusion fiable


La diffusion fiable est définie grâce à 3 critères, la validité, l’accord et l’intégrité :
validité lorsqu’un processus diffuse un message, tous les membres du groupe le délivrent ;
accord si un processus délivre un message, alors tous les autres membres du groupe le
délivrent ;
intégrité chaque message n’est délivré qu’une et une seule fois, après avoir été diffusé.

172
Communication de groupe (LP)

Le critère de validité doit se comprendre comme la garantie que si un processus


commence une diffusion fiable alors il conduira cette diffusion jusqu’au bout : il ne
peut pas envoyer son message à seulement une partie du groupe, le message sera
reçu, et donc délivré par tous. Ce critère garantit donc contre une défaillance en cours
de diffusion. Le critère d’accord, lui, concerne les risques de défaillance en cours de
traitement des messages reçus. Par exemple, un processus peut recevoir un message
et avoir une défaillance avant de le délivrer. Pour finir, le critère d’intégrité protège
contre la duplication des messages.
Si la topologie du réseau ne permet pas un envoi direct du message à diffuser de-
puis l’émetteur vers tous les membres du groupes, alors il est nécessaire d’utiliser un
algorithme de diffusion. L’algorithme 50 est un algorithme de diffusion fiable géné-
rique, exécuté par chaque processus Pi du groupe. Cet algorithme présente comment
il est possible d’assurer la fiabilité de l’envoi des messages en garantissant les 3 points
de la définition de la diffusion fiable.

Algorithme 50 : Algorithme de diffusion fiable


Variables locales à chaque processus Pi :
seqi [] : numéros de séquence des messages, init 0
Gi : groupe de diffusion, (Pi inclus)
Messages
DIF F U SION (m, num) : diffusion du message m au groupe
Règle 1 : Pi diffuse le message m au groupe :
begin
Pi envoi DIF F U SION (m, seqi [i]) à Pj , ∀Pj ∈ Gi
seqi [i] ← seqi [i] + 1
end
Règle 2 : Pi reçoit le message DIF F U SION (m, num) de Pj :
begin
if sender(m) = Pi then deliver(m)
else
if num > seq[j] then
envoie DIF F U SION (m) à Pk , ∀Pk ∈ Gi , Pk ̸= Pi ∧ Pk ̸= Pj
seqi [j] ← num
deliver(m)
end if
end if
end

Cet algorithme présente tout d’abord la préparation d’un message avant son envoi
à tous les membres d’un groupe par un algorithme de diffusion fiable. La fiabilité de
cette diffusion est assurée par l’exécution de l’algorithme de délivrance du message
par tous les membres du groupe. Le principe de l’algorithme de délivrance du mes-

173
Communication de groupe (LP)

sage est le suivant. Si le récepteur n’est pas l’émetteur alors le message est rediffusé à
tous ses voisins (sauf en direction de lui-même et de l’émetteur). Un grand nombre de
ces messages sont redondants et inutiles. En revanche, cette manière de faire assure
le bon fonctionnement de la diffusion en l’absence de connaissance sur la topologie
du groupe. En effet, cette étape a pour but d’atteindre les sommets les plus éloignés
de l’émetteur sur le graphe des liens de voisinage entre les membres du groupe. Par
exemple, si le graphe des communications est en forme de chaîne, le message transite
de membres du groupe en membres du groupe jusqu’à atteindre tous ses membres
le long de la chaîne, y compris le dernier. Une fois cette rediffusion faite en direction
de ses voisins, le message peut être délivré localement par le membre courant, toute
autre rediffusion du même message étant cette fois inutile. De cette manière, toute
topologie connexe du graphe des communications admet un diffusion fiable au sens
de la définition 7.4. En d’autres termes, tant qu’un sommet reste connecté aux autres
membres du groupe par au moins un voisin, il bénéficie de la fiabilité associée à la
communication de groupe telle que nous la définissons ici.
L’algorithme 50 permet de réaliser la diffusion fiable quelque soit le réseau utilisé.
Dans la suite nous illustrons les différents protocoles de diffusion fiable sur des ré-
seaux complets, où un processus peut communiquer directement avec un autre. Pour
les autres topologies de réseau, il sera nécessaire du coupler l’algorithme donné avec
l’aglorithme 50 pour arriver au même résultat.

7.3 Les ordres de délivrance des messages

Il existe différents types de diffusions fiables. On note :


— la diffusion FIFO : les messages envoyés par un même processus sont délivrés
dans le même ordre que l’ordre d’envoi ;
— la diffusion Causale : les messages sont délivrés dans un ordre compatible avec
le respect de la causalité ;
— la diffusion Atomique : les messages sont tous délivrés dans le même ordre.
Ces 3 types de diffusion correspondent respectivement aux protocoles FBCAST,
CBCAST et ABCAST qui seront abordés dans la section suivante.
C’est la condition de délivrance d’un message qui conditionne tel ou tel protocole
de diffusion fiable. Dans les algorithmes décrits ici, on fait donc attention à ne pas
confondre les deux instructions suivantes :
— receivep (m) : réception du message m par le processus p ;
— deliverp (m) : livraison du message m au processus p.
Dans tous les cas et de manière évidente, la réception précède toujours la déli-
vrance du message. On note :

receivep (m) → deliverp (m)

174
Communication de groupe (LP)

7.3.1 L’ordre FIFO (utilisé par FBCAST)

P1 P2 P3

temps

F IGURE 7.5 – Exemple d’exécution correcte d’une diffusion fiable de type FIFO.

Définition 7.5 L’ordre FIFO


Ordre FIFO : si un processus diffuse un message m1 avant de diffuser un message m2 ,
alors aucun processus ne délivre m2 à moins qu’il n’ait déjà délivré m1 .
Les messages envoyés par un processus p d’un groupe g sont délivrés dans l’ordre d’émis-
sion par tout processus q du groupe g. En d’autres termes :

∀(m, m′ ̸= m), sendp (m) → sendp (m′ ) ⇒ ∀q ∈ g, deliverq (m) → deliverq (m′ )

La figure 7.5 montre trois processus corrects au sens de la diffusion fiable respec-
tant l’ordre FIFO pour la délivrance des messages.

7.3.2 L’ordre causal (utilisé par CBCAST)

P1 P2 P3

temps

F IGURE 7.6 – Exemple d’exécution correcte d’une diffusion fiable respectant la causa-
lité.

Définition 7.6 L’ordre causal

175
Communication de groupe (LP)

Si le message m est la cause du message m′ (m′ est envoyé après la délivrance de m sur le
processus émetteur de m′ ) alors tous les processus délivrent le message m′ après le message m.
Ceci peut s’écrire de la manière suivante :

∀(m, m′ ̸= m), m → m′ ⇒ ∀q ∈ g, deliverq (m) → deliverq (m′ )

L’ordre de livraison des messages respecte la relation de causalité entre m et m’.

La figure 7.6 montre trois processus corrects au sens de la diffusion fiable respec-
tant l’ordre causale pour la délivrance des messages.

7.3.3 L’ordre atomique ou total (utilisé par ABCAST)

P1 P2 P3

temps

F IGURE 7.7 – Exemple d’exécution correcte d’une diffusion fiable atomique.

Définition 7.7 L’ordre atomique ou total


La relation d’ordre est étendue aux processus concurrents. En effet, si on a un groupe g,
tous les processus p de ce groupe délivrent les messages dans le même ordre, quelque soit l’ordre
l’émission des messages, d’où l’expression :

∀(m, m′ ̸= m), ∀p ∈ g, deliverp (m) → deliverp (m′ ) ⇒ ∀q ∈ g, deliverq (m) → deliverq (m′ )

La figure 7.7 montre trois processus corrects au sens de la diffusion fiable respec-
tant l’ordre total pour la délivrance des messages.

7.4 Protocoles

L’algorithme 50 ne prend pas en compte l’un ou l’autre des ordres de délivrance


cités dans la section précédente. Il faudra donc le compléter par un protocole de com-
munication spécifique pour garantir un ordre donné.

176
Communication de groupe (LP)

7.4.1 Le protocole FBCAST

Le protocole permet de délivrer les messages provenant d’un même processus


dans l’ordre de l’émission. Comme dans tous les protocoles de diffusion fiable, le
message est envoyé avec une estampille. Dans le cas de FBCAST, le message m est
accompagné du numéro de séquence d’émission #seq(m) et du numéro du processus
émetteur sender(m). Sur un processus émetteur, le numéro de séquence est initialisé à
1 puis est incrémenté à chaque envoi, il constitue donc une suite continue (sans trou)
de valeurs.

Le protocole

Chaque processus p sauvegarde le numéro de séquence next(q) du prochain mes-


sage à délivrer en provenance de chaque processus q du groupe g. Grâce à cette in-
formation, le processus p récepteur d’un message m est capable de délivrer ou non
le message en fonction de son numéro de séquence #seq(m). S’il correspond au pro-
chain message à délivrer (#seq(m) = next(q)), alors il délivre aussi dans l’ordre les
messages en provenance de q qui auraient été mis en attente. Initialement, les pro-
chains numéros de séquence des messages en provenance de tous les autres processus
ont la valeur 1 sur tous les processus du groupe (next(q) = 1). Le protocole est donc
le suivant à l’arrivée d’un message :

1. À la réception, sur p, d’un message m en provenance de q, m est stocké parmi


les messages attendant d’être délivrés ;
2. Tant qu’il existe un message m en provenance de q tel que le numéro de sé-
quence #seq(m) est égal au prochain numéro de séquence next(q), alors le
message m est délivré, supprimé de l’ensemble des messages à délivrer et le nu-
méro de séquence du prochain message à délivrer en provenance de q, next(q),
est incrémenté.

Ce protocole est relativement simple et peu contraignant. Il s’agit finalement de


maintenir un compteur des messages pour chaque membre du groupe et de vérifier
que les messages en provenance du même émetteur ne se doublent pas.

7.4.2 Le protocole CBCAST

Le protocole CBCAST permet une réduction du délai de diffusion fiable par rap-
port au protocole ABCAST, exposé plus en détail dans la section suivante. En effet, un
message peut être délivré dès sa réception, si les conditions le permettent.
Soit g un groupe de n processus Pi avec 1 ≤ i ≤ n. Lors de cette diffusion, l’ordre
causal des messages est conservé (voir section 7.3.2). Le protocole utilisé pour la dé-
livrance des messages permettant de garantir l’ordre causal s’appuie sur les horloges

177
Communication de groupe (LP)

vectorielles (voir section 7.1.5) et est donné ici. À noter que dans ce cas ce sont les mes-
sages auxquels nous attribuons des horloges et non pas les événements d’envoi et de
réception.

Le protocole

1. Avant d’envoyer m, le processus Pi incrémente Vi [i] et estampille le message


m;
2. À la réception d’un message m estampillé par V en provenance de Pi , le pro-
cessus Pj ̸= Pi diffère sa livraison jusqu’à ce que les conditions suivantes soient
réalisées :
(
1. Vj [i] = V [i] − 1
2. ∀k ∈ [1, n] et k ̸= i : Vj [k] ≥ V [k]
La condition 1. indique que le message qui arrive de i porte le numéro at-
tendu et donc qu’aucun message de i n’a été perdu. En effet, les messages qui
viennent d’un même expéditeur ont des liens de causalité.
3. Après livraison de m : Vj ← max(V, Vj )

Exemple

L’exemple donné à la figure 7.8 illustre d’une part la mise à jour des valeurs des
composantes des horloges vectorielles lors de la réception des messages et d’autre
part, le délai imposé aux messages dont l’estampille donnée à l’émission ne respecte
pas les critères de causalité. Le délai entre la réception et la délivrance du message est
matérialisé par un flèche en pointillé sur la figure.

(0,0,0) (0,0,0) (0,1,0) (0,1,1) (0,2,1) (0,3,1)


P1 receive receive deliver receive receive
deliver deliver deliver

(0,0,0) (0,1,1) (0,1,1) (0,3,1)


P2 receive
deliver (0,2,1) (0,3,1)

(0,0,0) receive
deliver
receive
deliver deliver
P3 receive

(0,1,0) (0,1,1) (0,1,1) (0,2,1) (0,3,1)

F IGURE 7.8 – Exemple du déroulement des diffusions fiables de type CBCAST.

7.4.3 Le protocole ABCAST

Afin de respecter l’ordre total (ou atomique) au niveau de la diffusion fiable AB-
CAST, on utilise un protocole de validation à deux phases. Dans une première phase

178
Communication de groupe (LP)

le message est diffusé à tous les membres du groupe puis une phase d’échange d’es-
tampilles temporaires a lieu pour associer une estampille définitive au message. Les
messages sont finalement délivrés par ordre d’estampille. Tous les processus associant
la même estampille à un même message, les messages sont délivrés dans le même
ordre sur chaque processus et respectent donc bien l’ordre de diffusion ABCAST. Ce
protocole coûteux prend 3n messages pour chaque diffusion.

Protocole

Soit g un groupe de n processus Pi avec 1 ≤ i ≤ n. Le protocole suivant


montre comment il est possible de construire l’estampille définitive pour délivrer
tous les messages dans le même ordre. Les estampilles utilisées, temporaires et dé-
finitive, sont des estampilles temporelles, contenant la datation du message calculée
à partir d’une horloge locale, à laquelle on ajoute le numéro du processus : ⟨date
d’é[Link]éro de l’émetteur⟩.

1. L’expéditeur Pi envoie le message à diffuser à tous les membres du groupe.


2. Chaque destinataire Pj met sa propre estampille temporaire au message reçu.
Celle-ci est plus grande que toutes les estampilles qu’il a reçues ou envoyées et
la renvoie à l’expéditeur Pi du message.
3. Quand l’expéditeur Pi a reçu toutes les réponses, il choisit la plus grande estam-
pille et envoie un message de validation contenant ce choix à tous les membres
du groupe.
4. Les messages validés sont alors délivrés aux applications dans l’ordre de leurs
estampilles.

Exemple

Avec ce protocole, lors d’une diffusion sans concurrence, c’est-à-dire lorsqu’un seul
processus diffuse un message et que tous les messages précédents ont déjà été délivrés,
il est facile de comprendre que le message sera diffusé correctement. Le message est
envoyé à tous les processus, qui lui associent une estampille, puis le processus émet-
teur collecte ces estampilles et envoie la valeur définitive au groupe. Puisqu’il n’y a
pas de concurrence, une fois l’estampille définitive reçue, les processus délivrent le
message et celui-ci respecte bien l’ordre ABCAST puisque tous les messages qui l’ont
précédé ont été délivrés d’après l’hypothèse de départ.
Le déroulement de l’algorithme présente donc plus d’intérêt lorsqu’il y a concur-
rence entre les messages. Dans l’exemple donné à la figure 7.9 nous illustreons la va-
lidation de trois diffusions fiables de type ABCAST au sein d’un groupe de trois pro-
cessus P1 , P2 et P3 . Ici, les processus ont des horloges qui ont des valeurs initiales de
14 pour P1 , 15 pour P2 et 16 pour P3 , suite aux précédentes diffusions. Les processus
diffusent respectivement les messages m1 pour , m2 et m3 de manière concurrente.

179
Communication de groupe (LP)

Nous supposons que les messages sont reçus dans un ordre différent pour chaque
processus. Nous pouvons noter ici que le message m1 a probablement été émis avant
le message m3 puisqu’il est enregistré avant le message m3 dans le processus P3

P1 P2 P3
m1 m3 m2 m2 m1 m3 m1 m3 m2
15.1 16.1 17.1 16.2 17.2 18.2 17.3 18.3 19.3
A A A A A A A A A

(a) Réception des messages sur les processus du groupe et mise à jour des estampilles provisoires.
Un message est envoyé de tous les processus du groupe vers le processus P1 avec leur estampille
provisoire pour ce message (en gras dans le tableau)

P1 P2 P3
m3 m2 m1 m2 m1 m3 m1 m3 m2
16.1 17.1 17.3 16.2 17.3 18.2 17.3 18.3 19.3
A A P A P A P A A

(b) Le processus P1 choisit la plus grande estampille et la diffuse (17.3). Cette estampille est définitive
et les messages sont alors prêts (noté P ) à être délivrés. Localement, ils sont réordonnés en fonction
des nouvelles estampilles. Ainsi, ils pourront être délivrés dès qu’ils auront la plus petite estampille
locale. Les processus diffusent maintenant les estampilles provisoires pour le message provenant de
P2 (en gras)

P1 P2 P3
m3 m1 m2 m1 m3 m2 m3 m2
16.1 17.3 19.3 17.3 18.2 19.3 18.3 19.3
A P P P A P A P

(c) Le message m1 est délivré sur le processus P3 . L’estampille définitive pour m2 est trouvée (19.3) et
est diffusée à tous les processus pour que les messages soient triés en fonction des estampilles en vue
d’éventuelles nouvelles délivrances de messages

P1 P2 P3
m1 m3 m2 m3 m2 m3 m2
17.3 18.3 19.3 18.3 19.3 18.3 19.3
P P P P P P P

(d) Des messages peuvent être délivrés suite au tri en fonction des estampilles. Le message m1 est
délivré sur le processus P2 . Suite à l’envoi sur P3 des estampilles provisoires du message m3 par tous
les processus du groupe, le processus P3 peut choisir l’estampille définitive pour son message (18.3).
Après diffusion de cette estampille, tous les messages sont prêts (P ) et sont délivrés dans l’ordre de la
liste triée des messages, la même pour tous les processus

F IGURE 7.9 – Exemple du déroulement d’une diffusion fiable de type ABCAST

La figure 7.9(a) montre l’état des processus après la réception de tous les messages.
Suivant le protocole, les messages sont mis en attente dans l’attente d’une estampille

180
Communication de groupe (LP)

définitive. Ici, les messages sont tous ordonnés dans l’ordre des estampilles, définitives
ou non. A chaque message est associé un état, A ou P qui signifie que le message est
en attente (A) d’une estampille définitive ou prêt (P ) à être délivré.
— P1 reçoit les messages dans l’ordre m1 , m3 et m2 . Il les place en attente et leur
attribue une estampille provisoire. La valeur de l’estampille de P1 étant 14, les
nouvelles estampilles deviennent pour m1 , m3 et m2 respectivement 15.1, 16.1
et 17.1.
— P2 reçoit les messages dans l’ordre m2 , m1 et m3 . Il les place en attente et leur
attribue une estampille provisoire. La valeur de l’estampille de P2 étant 15, les
nouvelles estampilles deviennent pour m2 , m1 et m3 respectivement 16.2, 17.2
et 18.2.
— P3 reçoit les messages dans l’ordre m1 , m3 et m2 . Il les place en attente et leur
attribue une estampille provisoire. La valeur de l’estampille de P3 étant 16, les
nouvelles estampilles deviennent pour m1 , m3 et m2 respectivement 17.3, 18.3
et 19.3.
Dans la phase de validation, les processus renvoient les nouvelles estampilles aux
processus émetteurs qui choisit la plus grande d’entre elles. La figure 7.9(a) illustre
l’exemple du processus P1 :
— P1 reçoit les estampilles 17.2 et 17.3 pour son message m1 . L’estampille défini-
tive est la plus grande des trois, c’est-à-dire e = max{16.1, 17.2, 17.3} = 17.3.
Sur la figure 7.9(b), cette estampille définitive a été envoyée à tous les processus
destinataires pour validation. Le message m1 est alors rangé dans l’ordre des estam-
pilles au niveau de chaque processus et marqué comme prêt à être délivré (P ). Sur
le processus P3 , le message m1 a la plus petite estampille de la liste des messages du
processus et il est en état P . Ce message peut donc être délivré, ce qui est visible sur
la figure 7.9(c), car les autres estampilles en attente ne peuvent pas voir leur valeur
décroître puisque le processus émetteur prendra le max des valeurs reçues, donc au
moins la valeur temporaire locale.
Le protocole se poursuit avec la validation des estampilles des messages m2 (fi-
gure 7.9(b)) et m3 (figure 7.9(c)). Sur la figure 7.9(d) les listes de messages des différents
processus sont ordonnées suivant l’ordre définitif des messages qui sont maintenant
tous prêts à être délivrés et le seront donc dans l’ordre m1 , m3 , m2 sur tous les proces-
sus.

7.5 Conclusion

Ce chapitre est l’occasion de montrer comment des communications de diffusion


fiables (validité, accord, intégrité) peuvent être réalisées à l’échelle d’un groupe de
processus ou processeurs. Nous voyons à cette occasion la difficulté de mise en œuvre
en raison de l’absence d’horloge globale d’une part et de la concurrence des exécutions
d’autre part. Comme nous le voyons dans ce chapitre, il est malgré tout possible de se
doter d’outils puissants permettant de garantir l’intégrité des applications distribuées
afin, par exemple, que la dépendance causale soit respectée.

181
Communication de groupe (LP)

7.6 Bibliographie

Pour la rédaction de ce chapitre je me suis principalement inspiré des ouvrages


suivants :
— “Distributed Systems, Principles and Paradigms” de A. Tanenbaum et S. Van
Steen, [Link]

182
Communication de groupe (LP)

183

Vous aimerez peut-être aussi