Exclusion Mutuelle Et Progrès
Exclusion Mutuelle Et Progrès
Lorsqu'un thread souhaite accéder à une file d'attente (ou une autre ressource
partagée), il doit attendre que le sémaphore correspondant soit disponible.
Exemple concret : Si deux threads tentent d'ajouter ou de retirer un élément d'une file
en même temps, seul un des threads obtiendra l'accès en premier grâce au sémaphore.
1/73
2. Progrès :
Le progrès garantit qu’aucun thread ou processus n’est bloqué de façon permanente si
des ressources sont disponibles.
Dans cette solution :
Tant qu’il y a des processus à traiter dans l’une des files (non vides), le thread du
processeur (ou consommateur) continuera à retirer et exécuter ces processus.
Aucun thread n'est bloqué de manière indéfinie tant que des processus existent
dans les files.
Exemple concret : Si la file d’attente 1 devient vide, le thread du processeur peut passer
à la file 2, évitant ainsi un blocage. Cela permet d'assurer la continuité des opérations.
3. Attente Bornée :
Cette règle garantit qu’un thread en attente de ressources partagées finira par obtenir
l'accès dans un délai raisonnable.
Dans cette solution :
Aucun thread ne sera bloqué indéfiniment car les sémaphores assurent une file
d'attente ordonnée et équitable (en fonction du mécanisme FIFO intégré aux
sémaphores).
En résumé
Exclusion Mutuelle : Un seul thread accède à une section critique à la fois grâce aux
sémaphores.
2/73
Progrès : Les threads continuent d'exécuter des processus tant qu’il y en a à traiter,
évitant ainsi les blocages.
Attente Bornée : Les threads en attente auront toujours la possibilité d’accéder aux
ressources sans rester bloqués indéfiniment.
Les concepts de base en systèmes d'exploitation sont essentiels pour comprendre leur
fonctionnement. Voici les principaux concepts :
Création et terminaison : Les processus sont créés (par exemple, par un utilisateur ou
un programme) et peuvent se terminer volontairement ou par l'OS.
États du processus :
3/73
3. Gestion de la mémoire :
L'OS gère la mémoire pour allouer les ressources nécessaires aux processus en cours
d'exécution.
Mémoire virtuelle : Utilise le disque dur comme extension de la RAM pour exécuter des
processus lorsque la mémoire est insuffisante.
Techniques :
Système de fichiers : Méthodes pour stocker et récupérer les données (par exemple,
FAT32, NTFS, EXT4).
Buffering et Spooling : Techniques pour gérer les données en attente d'utilisation par
les périphériques.
4/73
6. Gestion des ressources :
L'OS gère les ressources partagées entre les processus.
Exclusion mutuelle : Garantir qu’une seule entité accède à une ressource partagée à un
moment donné.
Autorisations : Contrôle les actions des utilisateurs sur les fichiers et ressources.
9. Systèmes multitâches :
Un système multitâche permet à plusieurs processus de s’exécuter simultanément en
partageant les ressources.
5/73
Préemptif : L’OS peut interrompre un processus pour en exécuter un autre.
Partage des ressources : Les systèmes partagent des fichiers, imprimantes, etc.
11. Virtualisation :
La virtualisation permet d’exécuter plusieurs systèmes d'exploitation sur une seule machine
physique via des machines virtuelles. Les hyperviseurs, comme VMware ou VirtualBox,
gèrent cette virtualisation.
1. Problème de la synchronisation :
6/73
La synchronisation est nécessaire lorsque plusieurs processus ou threads :
Doivent coordonner leurs actions pour éviter des erreurs comme des conditions de
course (race conditions), où l’ordre des exécutions impacte les résultats.
2. Section critique :
Une section critique est une partie d’un programme où des ressources partagées sont
accédées.
1. Exclusion mutuelle : Une seule entité (processus ou thread) doit accéder à la section
critique à la fois.
2. Progrès : Si plusieurs processus veulent accéder à une section critique et qu'elle est libre,
l’un d’eux doit pouvoir y entrer.
4. Mécanismes de synchronisation :
a. Sémaphores :
Un sémaphore est une variable entière utilisée pour contrôler l'accès aux ressources
partagées.
7/73
Types :
Sémaphore comptable : Peut avoir des valeurs supérieures à 1 (utile pour plusieurs
instances d’une ressource).
Opérations :
b. Verrous (Locks) :
Permettent de bloquer une ressource jusqu'à ce qu'elle soit libérée par le processus ou
thread qui l’utilise.
Types :
Verrou simple.
c. Moniteurs :
Une structure de données qui regroupe des variables partagées, leurs opérations, et un
mécanisme de synchronisation.
Les moniteurs permettent un accès sûr à une ressource grâce à des verrous implicites.
d. Variables conditionnelles :
Utilisées avec les moniteurs pour permettre à un processus d'attendre qu'une certaine
condition soit remplie.
Opérations :
e. Barrières :
8/73
Un mécanisme de synchronisation utilisé pour bloquer un groupe de threads jusqu'à ce
qu'ils aient tous atteint un point commun d'exécution.
Plusieurs processus (lecteurs) peuvent lire simultanément une ressource partagée, mais
un seul écrivain peut écrire à un moment donné.
Objectif : Assurer l’exclusion mutuelle pour les écrivains tout en permettant un accès
concurrentiel pour les lecteurs.
Les producteurs génèrent des données placées dans un tampon (buffer), et les
consommateurs retirent ces données pour traitement.
7. Défis en synchronisation :
9/73
Deadlock (Interblocage) : Lorsque deux ou plusieurs processus attendent
mutuellement des ressources, provoquant un blocage permanent.
Starvation (Famine) : Lorsqu’un processus attend indéfiniment parce que d'autres ont
toujours la priorité.
En résumé :
La synchronisation est un mécanisme essentiel pour éviter les erreurs dans les systèmes
multi-thread ou multi-processus. Elle repose sur des outils comme les sémaphores, verrous,
et moniteurs pour coordonner l'accès aux ressources partagées tout en respectant les
critères d'exclusion mutuelle, de progrès et d'attente bornée.
1. Ressource critique :
Une ressource critique est une ressource partagée entre plusieurs processus ou threads,
dont l'accès concurrent peut entraîner des problèmes si elle n'est pas correctement gérée.
10/73
Exemples de ressources critiques :
Assurer un accès contrôlé et ordonné à cette ressource pour éviter tout conflit.
2. Section critique :
Une section critique est une partie d’un programme qui accède à une ressource critique. Elle
représente le code où des conflits peuvent survenir si plusieurs processus ou threads
exécutent cette section en même temps.
Sensibilité : Elle contient des instructions qui modifient ou utilisent une ressource
critique.
Exigence d'exclusion mutuelle : Un seul processus ou thread à la fois doit exécuter cette
section pour éviter des erreurs.
// Section critique
void incrementer_compteur() {
11/73
compteur = compteur + 1; // Accès concurrent peut causer des problèmes
}
Définition Ressource partagée sensible aux accès Partie du code accédant à une
concurrents. ressource critique.
Exclusion Assurée en protégeant les sections Doit être respectée pour éviter des
Mutuelle critiques. conflits.
Verrous (Locks) : Assurent qu’un seul thread peut entrer dans une section critique à la
fois.
12/73
Exemple illustratif :
Supposons une ressource critique : un fichier journal où plusieurs processus écrivent des
données.
python
# Section critique
with open("[Link]", "a") as fichier:
[Link]("Nouvelle entrée\n")
Pour éviter que plusieurs processus écrivent en même temps dans le fichier (ce qui pourrait
entraîner des incohérences), on utilise des mécanismes comme un verrou autour de cette
section critique.
En résumé :
Une ressource critique est une entité partagée sensible à l'accès concurrent.
Une section critique est le bloc de code qui accède ou modifie cette ressource.
La synchronisation vise à protéger les sections critiques pour assurer un accès sécurisé
aux ressources critiques.
La problématique des sections critiques (Critical Section Problem, CSP) est un problème
fondamental en synchronisation, visant à garantir un accès sûr aux ressources partagées
dans les systèmes multitâches. Voici une vue d'ensemble des concepts de base, des types
existants, des solutions logicielles et matérielles, et des critères pour évaluer une solution.
13/73
1. Concepts de base
a. Section critique (Critical Section) :
Partie du programme où des ressources partagées sont accédées ou modifiées.
Le problème est de s'assurer qu'un seul processus ou thread exécute cette section à la
fois (exclusion mutuelle).
L'objectif est de synchroniser ces processus pour éviter des conditions de course (race
conditions) et des incohérences.
4. Section restante : Parties du code qui n'accèdent pas aux ressources partagées.
b. Problème à n processus :
Généralisation pour n processus ou threads.
14/73
A. Solutions logicielles
1. Basées sur des drapeaux (Flags) :
Fonctionnement :
2. Algorithmes de priorité :
Ajout d'une notion de priorité pour décider quel processus accède en premier à la
ressource.
3. Moniteurs :
Un sémaphore est une variable utilisée pour contrôler l'accès à une ressource.
B. Solutions matérielles
1. Instructions atomiques :
Fonctionnement :
2. Verrouillage matériel :
15/73
4. Critères pour vérifier une solution
Une solution au problème des sections critiques doit respecter les trois critères suivants :
a. Exclusion mutuelle :
Un seul processus à la fois peut être dans sa section critique.
b. Progrès (Progress) :
Si plusieurs processus souhaitent entrer dans leur section critique, le choix de celui qui
entre doit être effectué dans un délai fini.
16/73
Fonctionne pour deux processus.
Code :
// Processus i
flag[i] = true;
turn = 1 - i;
while (flag[1 - i] && turn == 1 - i) {
// Attendre
}
// Section critique
flag[i] = false;
Code :
c
// Section critique
while (test_and_set(&lock)) {
17/73
// Attendre
}
// Section critique
lock = false;
En conclusion, la résolution du problème des sections critiques est essentielle pour garantir
la sécurité et la cohérence dans les systèmes multitâches. Les solutions, qu'elles soient
logicielles ou matérielles, doivent respecter les critères d'exclusion mutuelle, de progrès et
d'attente bornée.
Pour garantir que les appels à acquire() et release() soient atomiques, il est essentiel
qu'ils soient exécutés sans interruption, de sorte que l'exécution de ces fonctions ne puisse
être préemptée ou affectée par un autre processus ou thread. Voici quelques points à
considérer et des techniques pour y parvenir :
Exemple problématique :
18/73
2. Techniques pour rendre les appels acquire() et
release() atomiques
A. Solutions matérielles
1. Instructions atomiques :
Test-and-Set
Compare-and-Swap
Fetch-and-Increment
Exemple (Test-and-Set) :
void acquire() {
while (test_and_set(&lock)) {
// Attendre
}
}
void release() {
lock = false;
}
Limitation :
19/73
Peut provoquer une latence si les interruptions restent désactivées trop
longtemps.
B. Solutions logicielles
1. Spinlocks (verrouillage actif) :
Une boucle d'attente active vérifie la disponibilité du verrou jusqu'à ce qu'il soit
libéré.
Exemple :
void acquire() {
while (__sync_lock_test_and_set(&lock, 1)) {
// Attendre
}
}
void release() {
__sync_lock_release(&lock);
}
3. Moniteurs ou Mutex :
20/73
synchronized(lock) {
// Section critique
}
Les API modernes comme pthread ou std::mutex utilisent des solutions hybrides,
combinant des techniques matérielles (instructions atomiques) et logicielles (attente
passive ou active).
2. État non-bloquant :
1. Atomicité :
Les deux fonctions doivent être exécutées sans interruption, garantissant qu'aucun
autre thread ne peut interférer avec leur exécution.
2. Exclusion mutuelle :
3. Attente bornée :
Les threads en attente d'un verrou doivent y accéder dans un délai fini.
4. Progrès :
Un thread ne peut pas être bloqué indéfiniment si aucune autre entité n'occupe le
verrou.
21/73
4. Exemple pratique (C avec pthread_mutex_t )
Code :
c
#include <pthread.h>
pthread_mutex_t lock;
void acquire() {
pthread_mutex_lock(&lock);
}
void release() {
pthread_mutex_unlock(&lock);
}
int main() {
pthread_mutex_init(&lock, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&lock);
return 0;
}
22/73
En conclusion, pour garantir que acquire() et release() soient atomiques, on peut
s'appuyer sur des instructions matérielles atomiques ou des primitives logicielles fiables
comme les mutex ou les sémaphores. Le choix dépend du niveau d'abstraction (logiciel ou
matériel) et des exigences de performance.
2/2
Compare-and-Swap
Compare-and-Swap (CAS) est une instruction atomique utilisée dans les systèmes
concurrents pour mettre à jour une valeur partagée de manière sûre sans utiliser de verrou
explicite. C'est un outil essentiel dans les implémentations de structures de données non
bloquantes (lock-free). Voici une explication détaillée de son fonctionnement, son
importance, ses avantages et ses inconvénients.
1. Fonctionnement de Compare-and-Swap
L'instruction CAS compare la valeur d'une variable mémoire à une valeur attendue. Si elles
sont égales, elle met à jour la variable avec une nouvelle valeur. Sinon, elle ne fait rien.
Pseudo-code de CAS :
Paramètres :
23/73
new_value : La valeur à laquelle on veut mettre à jour la variable.
Retour :
2. Exemple pratique
Problème :
#include <stdatomic.h>
#include <stdbool.h>
int main() {
int counter = 0;
increment(&counter); // Thread 1
increment(&counter); // Thread 2
printf("Counter: %d\n", counter); // Résultat : 2
return 0;
}
Explications :
24/73
Si la valeur n'a pas changé pendant le calcul, CAS met à jour counter à old_value +
1.
3. Importance et applications
Structures de données lock-free :
CAS est couramment utilisé pour implémenter des piles, des files d'attente, des
listes chaînées, etc., sans utiliser de verrou.
Synchronisation fine :
Permet de gérer des ressources partagées sans bloquer des threads inutiles.
4. Propriétés de CAS
Propriété Description
Atomicité Garantit que la comparaison et la mise à jour sont effectuées en une seule opération
atomique.
5. Avantages
1. Pas de blocage :
25/73
CAS évite les problèmes de blocage (deadlock) qui peuvent survenir avec des
verrous traditionnels.
2. Performances élevées :
3. Équité :
Tous les threads peuvent tenter leur chance pour accéder à la ressource.
4. Simple à utiliser :
6. Inconvénients
1. Boucles d’attente active (Spin) :
CAS fonctionne mieux pour des opérations rapides. Pour des sections critiques
complexes, les verrous peuvent être plus adaptés.
7. Support matériel
La plupart des processeurs modernes prennent en charge l'instruction CAS ou des
équivalents, souvent sous les noms suivants :
x86 : CMPXCHG
26/73
PowerPC : Compare-And-Swap
Flexibilité Plus flexible pour les mises à jour Limité aux mécanismes de
conditionnelles. verrouillage.
Effet sur les Réduit les conflits inutiles. Peut entraîner plus de
performances contention.
9. Conclusion
Le Compare-and-Swap (CAS) est une instruction atomique puissante et flexible qui joue un
rôle clé dans la programmation concurrente moderne. Bien qu'elle présente des
inconvénients dans certains scénarios, elle est indispensable pour créer des structures de
données efficaces et non bloquantes. Le choix d'utiliser CAS ou une autre méthode dépend
du contexte, de la complexité de l'opération et des exigences de performance.
Critical Region
27/73
1. Caractéristiques d'une région critique
1. Accès exclusif :
Une seule entité (processus ou thread) doit être autorisée à exécuter la région
critique à un moment donné.
Une région critique implique l'accès à des données ou ressources partagées, comme
des variables globales, des fichiers, ou des périphériques matériels.
Des mécanismes comme des verrous (locks), des sémaphores ou des moniteurs
sont utilisés pour réguler l'accès à la région critique.
2. Deadlock (blocage) :
3. Starvation (famine) :
Un thread n'obtient jamais l'accès à la région critique parce que d'autres threads
sont toujours prioritaires.
28/73
3. Conditions pour gérer une région critique
Pour gérer correctement une région critique, une solution doit respecter les critères suivants
:
1. Exclusion mutuelle :
2. Progrès :
Si aucun thread n'est dans la région critique, un thread souhaitant y entrer doit
pouvoir le faire rapidement.
3. Attente bornée :
Un thread demandant l'accès à une région critique doit y accéder dans un délai fini.
A. Logiciels
1. Locks (verrous) :
Utilisation explicite de verrous pour s'assurer qu'un seul thread entre dans la région
critique.
Exemple en C :
c
pthread_mutex_t lock;
void critical_region() {
pthread_mutex_lock(&lock);
// Code de la région critique
pthread_mutex_unlock(&lock);
}
2. Sémaphores :
Un sémaphore peut être utilisé pour contrôler l'accès à la région critique via des
opérations P() et V() .
Exemple :
29/73
c
3. Moniteurs :
Une structure haut niveau encapsule la région critique et contrôle l'accès via des
primitives comme wait() et signal() .
4. Algorithmes logiciels :
B. Matériels
1. Instructions atomiques :
while (test_and_set(&lock)) {
// Attendre
}
// Code de la région critique
lock = false; // Libérer le verrou
Les interruptions peuvent être désactivées pour empêcher tout autre thread ou
processus de perturber la région critique.
5. Exemple illustratif
30/73
Supposons deux threads qui incrémentent une variable globale.
int counter = 0;
void increment() {
counter++; // Non sécurisé
}
Si les deux threads exécutent counter++ simultanément, le résultat final peut être
incorrect à cause de l'incrémentation non atomique.
pthread_mutex_t lock;
void increment() {
pthread_mutex_lock(&lock);
counter++; // Sécurisé
pthread_mutex_unlock(&lock);
}
31/73
Méthode Avantages Inconvénients
Désactivation des Efficace pour les sections courtes. Non applicable en mode
interruptions utilisateur.
7. Conclusion
Une Critical Region est une partie essentielle des systèmes multitâches et doit être protégée
pour éviter des conflits ou des incohérences. Le choix de la méthode de synchronisation
dépend du contexte, des ressources partagées, et des exigences en termes de performance
et de complexité.
Penser
Manger
Représentation du problème :
Les philosophes représentent des processus.
32/73
2. Deadlock dans ce problème
Comment un deadlock peut survenir ?
1. Chaque philosophe prend d'abord la fourchette à gauche de sa place.
Exemple de scénario :
Chaque philosophe attend maintenant que son voisin libère une fourchette pour
pouvoir manger. Cela ne se produit jamais, donc un deadlock se produit.
Une fourchette ne peut être tenue que par un seul philosophe à la fois.
3. No Preemption (Non-préemption) :
Une fourchette ne peut pas être forcée de revenir à son état libre.
33/73
Une chaîne circulaire de philosophes attend des ressources détenues par leurs
voisins.
Exemple :
// Manger
pthread_mutex_unlock(&forks[left]);
pthread_mutex_unlock(&forks[right]);
}
34/73
Limiter le nombre de philosophes autorisés à prendre les fourchettes.
Utiliser un sémaphore global pour garantir qu’au moins une fourchette reste
disponible.
sem_t max_diners;
// Manger
pthread_mutex_unlock(&forks[left]);
pthread_mutex_unlock(&forks[right]);
sem_post(&max_diners); // Libérer la permission de manger
}
B. Approches sophistiquées
1. Un philosophe peut prendre les deux fourchettes simultanément :
Les philosophes demandent les deux fourchettes à l’arbitre, qui gère l’accès.
Exemple :
35/73
pthread_mutex_t arbiter;
pthread_mutex_lock(&arbiter);
pthread_mutex_lock(&forks[left]);
pthread_mutex_lock(&forks[right]);
pthread_mutex_unlock(&arbiter);
// Manger
pthread_mutex_unlock(&forks[left]);
pthread_mutex_unlock(&forks[right]);
}
6. Conclusion
Le problème des philosophes illustre les défis pratiques des deadlocks dans la gestion des
ressources partagées. Plusieurs solutions existent pour éviter ces blocages, mais le choix de
la méthode dépend des contraintes et des exigences du système (performance, simplicité,
36/73
etc.). Une compréhension claire des conditions de deadlock permet de concevoir des
systèmes robustes et fiables.
Description du problème
1. Tampon partagé : Il existe un tampon de taille finie (ou un buffer), où les producteurs
déposent des éléments et où les consommateurs les retirent.
4. Problèmes potentiels :
5. Objectif : Assurer une synchronisation correcte pour éviter les erreurs mentionnées ci-
dessus tout en permettant aux producteurs et consommateurs de fonctionner en
parallèle.
37/73
1. Conditions de synchronisation pour une région critique
Pour résoudre ce problème, nous devons respecter trois conditions :
3. Attente bornée : Aucun thread ne doit attendre indéfiniment pour accéder au tampon.
Variables nécessaires :
Tampon : Une structure de données pour stocker les éléments (par exemple, un tableau
circulaire).
Index : Variables pour suivre la première position libre (pour les producteurs) et la
première position occupée (pour les consommateurs).
Sémaphores :
Un mutex mutex pour garantir une exclusion mutuelle lors de l'accès au tampon.
Initialisation :
38/73
// Producteur
void producer() {
while (true) {
// Générer un élément à ajouter au tampon
item new_item = produce_item();
// Consommateur
void consumer() {
while (true) {
// Attendre qu'il y ait un élément à consommer
sem_wait(&full); // Attente sur le sémaphore full
39/73
pthread_mutex_unlock(&mutex);
// Consommer l'élément
consume_item(consumed_item);
3. Explication du code
1. Exclusion mutuelle :
Le mutex mutex garantit que l'accès au tampon est exclusif, donc un seul thread
(producteur ou consommateur) peut modifier le tampon à la fois.
3. Progrès :
4. Attente bornée :
Les sémaphores garantissent que les threads ne sont jamais bloqués indéfiniment.
Chaque thread aura une chance de s'exécuter en fonction de la disponibilité des
ressources.
40/73
Critère État avec la solution proposée
Progrès Les producteurs et les consommateurs peuvent continuer dès qu'il y a de la place
ou des éléments disponibles.
Attente bornée Le sémaphore empêche l'attente infinie ; chaque thread aura une chance
d'accéder à la ressource.
6. Conclusion
Le problème du tampon limité est une illustration classique des concepts de région
critique, synchronisation et mutuelle exclusion en programmation concurrente.
L'utilisation de sémaphores et de mutexes permet de gérer efficacement l'accès concurrent
à des ressources partagées, garantissant ainsi l'intégrité des données et le bon
fonctionnement du système.
Monitors
Monitors
Monitors
41/73
Un monitor (moniteur en français) est une abstraction de haut niveau utilisée pour la
synchronisation et la gestion de l'accès aux ressources partagées dans les systèmes
concurrentiels. Il est conçu pour simplifier la gestion des sections critiques en fournissant
une manière structurée de contrôler l'accès simultané à des ressources partagées tout en
garantissant la mutual exclusion (exclusion mutuelle) et la synchronisation.
Les moniteurs sont utilisés dans des langages de programmation comme Java ou Erlang, et
font partie d'une approche plus moderne de la synchronisation par rapport aux techniques
basées sur des verrous explicites, comme les mutexes et sémaphores.
1. Variables internes :
Les variables partagées qui sont protégées par le moniteur (par exemple, un
tampon partagé, une liste, etc.).
2. Procédures ou méthodes :
Les opérations que les threads peuvent appeler pour manipuler les variables
partagées. Ces méthodes sont l'unique moyen pour les threads d’interagir avec les
42/73
ressources protégées.
3. Conditions :
wait() : Un thread appelle cette méthode pour suspendre son exécution, libérer le
verrou du moniteur et attendre qu'une condition spécifique soit remplie.
signal() ou notify() : Un thread appelle cette méthode pour réveiller un autre thread
en attente.
Un seul thread peut exécuter une procédure dans le moniteur à la fois, garantissant
ainsi que l'accès aux variables partagées est exclusif.
Grâce aux conditions (comme wait() et signal() ), les threads peuvent être
synchronisés de manière flexible, en fonction de l'état des variables partagées.
Sécurité :
L’utilisation de moniteurs aide à éviter les erreurs courantes dans les systèmes
concurrents, telles que les conditions de course, les deadlocks, et les starvations,
en gérant explicitement l'accès concurrent.
43/73
blocs de code est réalisée à l’aide du mot-clé synchronized .
java
class Counter {
private int count = 0;
Explication :
Le mot-clé synchronized sur les méthodes garantit que seul un thread peut exécuter
increment() ou getCount() à la fois, empêchant ainsi les conditions de course.
Si un thread appelle increment() , tout autre thread qui appelle l'une des méthodes
synchronisées doit attendre que le premier termine son exécution.
java
class BoundedBuffer {
private int[] buffer = new int[10];
private int count = 0;
private int in = 0;
private int out = 0;
44/73
// Méthode pour ajouter un élément dans le tampon
public synchronized void produce(int item) throws InterruptedException {
while (count == [Link]) {
wait(); // Attendre si le tampon est plein
}
buffer[in] = item;
in = (in + 1) % [Link];
count++;
notifyAll(); // Réveiller les consommateurs
}
Explication :
45/73
Simplicité :
Dans certains cas, l'utilisation des moniteurs peut entraîner une surcharge en raison
de la gestion des conditions et de l'accès exclusif.
Verrouillage global :
Verrouillage fin :
Les moniteurs sont généralement utilisés pour des verrous de niveau global sur un
objet. Cela peut être trop restrictif pour certaines situations où un verrouillage plus
granulaire est nécessaire.
8. Conclusion
Les moniteurs sont une abstraction puissante pour gérer la synchronisation dans les
systèmes concurrents. En encapsulant les données partagées et en fournissant un
46/73
mécanisme intégré de gestion des verrous et des conditions, les moniteurs rendent la
programmation concurrente plus facile, plus sécurisée, et plus lisible. Toutefois, ils doivent
être utilisés judicieusement pour éviter des problèmes de performances dans des systèmes
très concurrentiels.
definition de thread
2/2
Définition de Thread
Un thread (ou fil d'exécution en français) est la plus petite unité d'exécution d'un
programme. Un thread permet à un programme de réaliser plusieurs tâches simultanément
en partageant les mêmes ressources du programme, comme la mémoire et les variables
globales, mais en exécutant des instructions indépendantes.
2. Contexte d'exécution :
Chaque thread a son propre contexte d'exécution, mais il partage une grande
partie de l'état du programme principal (comme la mémoire du programme, les
fichiers ouverts, etc.).
47/73
4. Indépendance :
Les threads d'un programme s'exécutent indépendamment les uns des autres.
Cependant, dans un programme multithreadé, plusieurs threads peuvent travailler
sur des tâches différentes simultanément, ce qui améliore la performance du
programme, notamment sur des systèmes multiprocesseurs.
Types de Threads
1. Threads Utilisateur (User Threads) :
Ce sont des threads gérés directement par le système d'exploitation. Chaque thread
utilisateur est mappé à un thread noyau, et c'est l'OS qui planifie leur exécution. Ces
threads peuvent être exécutés sur différents processeurs dans un système
multiprocesseur.
3. Threads Hybrides :
Ce modèle combine les threads utilisateur et noyau, où certains threads sont gérés
par la bibliothèque d'exécution, mais le système d'exploitation prend aussi en
charge certains aspects de leur gestion.
48/73
2. Partage des ressources :
Les threads d'un même programme peuvent partager les mêmes ressources
mémoire et variables globales, ce qui facilite la communication entre eux par
rapport aux processus indépendants.
3. Réactivité :
Lorsque plusieurs threads accèdent à des ressources partagées sans une gestion
correcte de la synchronisation, cela peut entraîner des résultats imprévisibles. Par
exemple, deux threads qui modifient une même variable en même temps peuvent
causer des incohérences dans les données.
2. Deadlock (Interblocage) :
Un ou plusieurs threads peuvent se voir refuser l'accès aux ressources par un autre
thread indéfiniment, ce qui peut entraîner leur "famine" (starvation).
49/73
Exemple avec Thread :
java
java
Conclusion
Un thread est une unité fondamentale d'exécution dans un programme. La gestion des
threads est essentielle pour créer des applications multithreadées performantes et
réactives. L'utilisation des threads permet de tirer parti des systèmes multiprocesseurs et
d'améliorer la réactivité des applications en permettant à plusieurs tâches de s'exécuter en
50/73
parallèle. Cependant, leur gestion doit être effectuée avec précaution pour éviter des
problèmes tels que les conditions de course, les deadlocks, et la starvation.
2/2
Principe
Un thread ou un processus vérifie constamment une condition dans une boucle, en utilisant
des instructions répétées (comme while ) pour déterminer si la ressource ou l'état qu'il
attend devient disponible. Pendant cette vérification, le thread ne fait aucun travail utile, ce
qui peut entraîner une consommation inutile de ressources processeur.
Exemple
Un exemple classique de temporisation active est une boucle while qui vérifie si une
variable partagée est mise à jour :
while (flag == 0) {
// Boucle d'attente active
}
51/73
Problèmes de la Temporisation Active
1. Utilisation inefficace du processeur :
Le thread en attente consomme du temps CPU sans accomplir de travail utile, ce qui
peut ralentir l'ensemble du système, surtout dans les environnements à ressources
limitées.
2. Scalabilité limitée :
Lorsque le coût de passer d’un état d’exécution à un état suspendu est plus élevé
que celui de rester actif (par exemple, dans les systèmes en temps réel ou avec des
délais très courts).
Dans certains cas, une attente active peut être utilisée pour éviter des conditions
complexes comme les interblocages.
52/73
Solutions Alternatives
Pour éviter les inconvénients de la temporisation active, plusieurs alternatives existent :
1. Sémaphores ou Mutex :
2. Signaux ou Interruptions :
Les threads ou processus peuvent être réveillés lorsqu'une condition est remplie via
un signal ou une interruption.
Exemple :
while (flag == 0) {
sleep(1); // Pause de 1 seconde avant de vérifier à nouveau
}
Les threads peuvent attendre une condition spécifique avec des mécanismes
comme wait() et notify() dans les moniteurs.
53/73
1. Utilisation de délais :
Exemple :
while (flag == 0) {
sched_yield(); // Cède le processeur à d'autres threads
}
2. Réduction de la priorité :
Exécuter le thread en attente active avec une priorité plus faible pour minimiser son
impact sur les autres tâches du système.
Conclusion
La temporisation active est une solution simple mais inefficace pour gérer la synchronisation
dans les systèmes concurrents. Elle est généralement remplacée par des solutions plus
avancées comme les sémaphores, les mutex, ou les interruptions pour améliorer l'efficacité
et la scalabilité. Toutefois, elle peut être utile dans des situations spécifiques où la latence et
la simplicité priment sur l'efficacité.
54/73
Etat : Tableau[0..1] de Booléen.
Le tableau est initialisé à Faux. Si Etat[i] est Vrai, cette
valeur indique que le processus Pi est prêt pour
entrer en SC.
L’algorithme de la solution est : expliquer
Principe
Une variable entière partagée turn est utilisée pour indiquer quel processus est
autorisé à entrer en SC.
Algorithme
1. Entrée en SC :
2. Sortie de SC :
Code Pseudocode
Pour le processus Pi :
55/73
while (turn != i) {
// Attente active
}
// Section critique (SC)
// ...
turn = 1 - i; // Permet à l'autre processus d'entrer
Problèmes de la Solution 1
2. Équité limitée :
Cette solution impose une alternance stricte entre les processus. Si un processus n'a
pas besoin d'accéder à la SC, l'autre doit quand même attendre son tour, même si la
SC est libre.
Principe
On remplace la variable turn par un tableau Etat qui mémorise l'intention de chaque
processus d'entrer en SC.
Etat[i] = Faux signifie que le processus Pi n'est pas intéressé par la SC pour le
moment.
56/73
Algorithme
1. Entrée en SC :
Il vérifie ensuite si l'autre processus (P1−i ) n'est pas dans un état où il veut entrer en
SC.
2. Sortie de SC :
Code Pseudocode
Pour le processus Pi :
État des processus Ne tient pas compte des intentions Suit les intentions des processus
Conclusion
57/73
La Solution 2 (Etat) est une amélioration significative par rapport à la Solution 1 (turn), car
elle permet de mieux gérer l'état des processus et de garantir le progrès. Cependant, elle
repose toujours sur une forme de temporisation active, ce qui peut être inefficace. Des
solutions avancées, comme les sémaphores ou les moniteurs, permettent d'éliminer ce
problème.
Étapes de Fonctionnement
1. Déclaration d'intention :
58/73
Pi vérifie si l'autre processus (Pj ) est prêt à entrer dans sa SC en consultant
Etat[j] .
Sinon, Pi reste bloqué dans une boucle d'attente active jusqu'à ce que Etat[j]
devienne Faux.
3. Sortie de la SC :
Lorsqu'il termine sa SC, Pi remet Etat[i] = Faux , signalant qu'il ne souhaite plus
Scénario Problématique
SC :
P0 fixe
Etat[0] = Vrai .
P1 fixe
Etat[1] = Vrai .
P0 voit que
Etat[1] = Vrai et entre dans une boucle d'attente.
P1 voit que
Etat[0] = Vrai et entre également dans une boucle d'attente.
3. Aucun des deux processus ne remet son état à Faux, car ils attendent indéfiniment que
l'autre processus le fasse.
Solution au Problème
Le problème se produit en raison d'un manque de coordination entre les deux processus
lorsque les deux déclarent simultanément vouloir entrer dans la SC. Une amélioration
courante consiste à introduire un mécanisme de priorité pour briser cette situation
d'interblocage.
59/73
L'algorithme de Peterson ajoute une variable supplémentaire appelée turn pour gérer les
priorités entre les deux processus. Voici le principe :
1. Chaque processus continue d'utiliser le tableau Etat pour signaler son intention
d'entrer dans la SC.
2. Une variable turn indique quel processus doit avoir la priorité en cas de conflit.
Algorithme de Peterson
Pour le processus Pi :
2. Par exemple :
Une fois que P1 termine sa SC, il remet Etat[1] = Faux , permettant ainsi à P0
d'entrer.
3. Cela évite la situation de boucle infinie, car au moins un des processus pourra
progresser.
60/73
Conclusion
Dans la solution initiale, bien que l'exclusion mutuelle soit respectée, elle est vulnérable au
problème de blocage mutuel. En ajoutant une variable de priorité comme dans l'algorithme
de Peterson, on peut garantir :
Critical Region
Définition
Une région critique est une section du code dans un programme où plusieurs processus ou
threads accèdent simultanément à des ressources partagées, telles que des variables, des
fichiers ou des périphériques. L'objectif est de s'assurer que l'accès concurrent à ces
ressources est sûr et cohérent.
Dans une région critique, il est crucial d'éviter des situations telles que :
1. Exclusion Mutuelle :
Une seule entité (processus ou thread) peut accéder à la région critique à la fois.
2. Progrès :
Si plusieurs processus souhaitent entrer dans la région critique, l'un d'eux doit être
autorisé à entrer dans un délai raisonnable.
61/73
3. Attente Bornée :
4. Indépendance de la vitesse :
Problèmes Associés
1. Deadlock (Interblocage) :
2. Starvation (Famine) :
3. Conditions de concurrence :
1. Solutions Logicielles
Exemples :
Algorithme de Dekker : Utilise des indicateurs pour assurer l'exclusion mutuelle dans un
environnement à deux processus.
62/73
2. Solutions Matérielles
Exemples :
Test-and-Set : Une instruction atomique qui vérifie et modifie une variable en une seule
opération.
Compare-and-Swap : Une autre instruction atomique utilisée pour gérer les ressources
critiques.
3. Mécanismes Système
Les systèmes d'exploitation fournissent des outils pour gérer les régions critiques,
souvent basés sur des primitives de synchronisation.
Exemples :
Exemple
Supposons deux processus qui doivent accéder à une variable partagée :
int counter = 0;
void increment() {
63/73
counter = counter + 1; // Problème si exécuté simultanément par deux processus
}
int counter = 0;
pthread_mutex_t lock;
void increment() {
pthread_mutex_lock(&lock); // Verrouillage
counter = counter + 1; // Accès sécurisé
pthread_mutex_unlock(&lock); // Déverrouillage
}
Conclusion
Une région critique est essentielle pour assurer une coordination correcte entre les
processus ou threads qui accèdent à des ressources partagées. Grâce à des mécanismes
logiciels et matériels adaptés, les risques d'incohérence des données, d'interblocages ou de
famine peuvent être minimisés.
La Critical Region (région critique) et la Section Critique (SC) sont deux concepts proches en
programmation concurrente, mais ils diffèrent par leur portée et leur formalisation. Voici
une comparaison claire entre les deux :
1. Définition
Termes Critical Region Section Critique
Définition C'est un concept plus général pour gérer Une partie spécifique d'un programme
l'accès concurrent aux ressources où une ressource partagée est modifiée
64/73
Termes Critical Region Section Critique
partagées. ou utilisée.
Portée En général, il s'agit d'une méthode ou d'une Il s'agit d'une portion de code
structure intégrée pour protéger les spécifique qui doit être protégée contre
ressources partagées. l'accès concurrent.
2. Niveau d'Abstraction
Critical Region Section Critique
Fournit un cadre abstrait pour gérer la synchronisation, Désigne une zone de code concrète où
souvent via des mécanismes comme des moniteurs. une ressource partagée est manipulée.
3. Concept vs Implémentation
Critical Region Section Critique
Implémente le concept d'exclusion mutuelle Nécessite une gestion explicite par le programmeur,
avec des abstractions de haut niveau (ex. : souvent avec des primitives comme des mutex ou des
moniteurs). sémaphores.
4. Exemples
Critical Region
Décrit dans un cadre structuré ou via des mécanismes comme les moniteurs :
monitor ExampleMonitor {
void accessCriticalResource() {
// Gestion automatique de l'accès à la ressource partagée
65/73
}
}
Section Critique
Partie précise de code manipulant une ressource partagée, protégée avec un mutex ou
un sémaphore :
pthread_mutex_lock(&mutex);
// Section Critique
sharedResource++;
pthread_mutex_unlock(&mutex);
Abstraction Approche de haut niveau pour gérer les Nécessite une gestion explicite par le
ressources. programmeur.
Sécurité Généralement gérée par le système (ex. : Dépend des primitives utilisées (mutex,
moniteurs). sémaphores).
Complexité Moins complexe pour le programmeur. Peut nécessiter une gestion manuelle
complexe.
Résumé
Point clé Critical Region Section Critique
66/73
Point clé Critical Region Section Critique
En résumé, une Critical Region est un concept abstrait qui peut inclure des Sections
Critiques, mais elle les gère automatiquement à un niveau supérieur d'abstraction. Une
Section Critique, en revanche, est une zone concrète du programme qui nécessite une
gestion explicite pour éviter les conflits.
Synchronization
In Window
❏ Uses interrupt masks to protect access to global
resources on uniprocessor systems
❏ Uses spinlocks on multiprocessor systems
❏ Spinlocking-thread will never be preempted expilquer
en francis
2/2
67/73
Windows utilise des masques d'interruption pour protéger l'accès aux ressources
globales.
Comment ça fonctionne :
Cela garantit une exclusion mutuelle, mais est limité aux systèmes à processeur
unique.
Spinlocks :
Windows offre des dispatcher objects dans l'espace utilisateur, qui fonctionnent comme
des mécanismes de synchronisation.
Ces objets incluent :
Mutex (Mutual Exclusion) : Garantit qu'un seul thread accède à une ressource
partagée.
68/73
Minuteries (Timers) : Notifications aux threads après l'expiration d'un délai spécifié.
4. Événements (Events) :
Les événements dans Windows sont comparables aux variables de condition utilisées
en programmation concurrente.
5. Minuteries (Timers) :
Elles sont utilisées pour planifier des tâches ou pour des mécanismes de
synchronisation basés sur le temps.
Résumé
Systèmes uniprocesseurs : Utilisation de masques d'interruption pour empêcher les
conflits lors de l'accès à des ressources partagées.
69/73
Événements et minuteries : Facilitent la coordination et la synchronisation des threads
en fonction des états (signalé/non signalé) ou du temps.
Synchronization
In Linux
❏ Linux:
❏ Prior to kernel Version 2.6, disables interrupts
to implement short critical sections
❏ Version 2.6 and later, fully preemptive
❏ Linux provides:
❏ Semaphores
❏ atomic integers
❏ Spinlocks
❏ reader-writer versions of both
❏ Critical regions
❏ On single-cpu system, spinlocks replaced by expliquer
en francis
Pourquoi ?
Cette méthode était efficace pour les systèmes uniprocesseurs, car il n'y avait qu'un
seul processeur à gérer.
70/73
2. Synchronisation dans les versions 2.6 et ultérieures
Depuis la version 2.6, Linux est devenu entièrement préemptif :
Cela signifie qu'un thread ou un processus peut être interrompu à presque tout
moment, même dans le noyau, sauf dans des cas particuliers comme les spinlocks.
a. Sémaphores
Les sémaphores sont utilisés pour contrôler l'accès à une ressource partagée par
plusieurs threads ou processus.
Utile pour les scénarios où plusieurs threads doivent accéder à une ressource, mais
en nombre limité.
b. Entiers atomiques
Ces opérations sont atomiques : elles se déroulent entièrement ou pas du tout, sans
être interrompues.
Ils fonctionnent en faisant "tourner" un thread dans une boucle jusqu'à ce qu'il
obtienne le verrou.
71/73
Approprié pour des sections critiques très courtes, car il empêche le thread d'être
interrompu.
Utile lorsque les lectures sont fréquentes, mais les écritures rares.
e. Régions critiques
Une région critique est une zone de code où des ressources partagées sont modifiées.
Linux gère ces régions critiques via des mécanismes comme les spinlocks ou les
sémaphores.
Les spinlocks sont remplacés par des mécanismes plus simples, comme la
désactivation des interruptions.
Pourquoi ?
Résumé
Caractéristiques Avant Linux 2.6 Depuis Linux 2.6
72/73
Caractéristiques Avant Linux 2.6 Depuis Linux 2.6
73/73