Ce document académique présente la correction détaillée du troisième Travail Pratique (TP N°3) du module "Systèmes d'Exploitation Avancés", spécifiquement conçu pour les étudiants de première année ingénieur. Il vise à consolider leur compréhension des mécanismes fondamentaux de synchronisation des threads et processus.
Ce TP couvre les notions essentielles suivantes, illustrées par des implémentations concrètes en langage Java :
- La conception et la mise en œuvre d'une barrière de synchronisation à l'aide de sémaphores.
- La résolution du problème classique du Producteur/Consommateur via l'utilisation appropriée des sémaphores.
TP N°3 : Correction des Exercices sur la Synchronisation des Threads en Java
Ce document présente la correction des exercices du Travaux Pratiques (TP) n°3, centré sur l'utilisation des sémaphores pour la synchronisation des threads en Java. Il s'inscrit dans le cadre du module "Systèmes d'Exploitation Avancés" pour les étudiants de 1ère année Ingénieur, et a été élaboré par Mme Lilia SFAX durant l'année universitaire 2010-2011.
Barrière de Synchronisation avec Sémaphores
Une barrière de synchronisation est un mécanisme qui assure que tous les threads d'un groupe atteignent un point d'exécution commun avant qu'aucun d'entre eux ne puisse continuer. C'est crucial pour des opérations où plusieurs tâches parallèles doivent se coordonner à des étapes spécifiques.
Classe Processus
Cette classe modélise un thread qui simule une activité avant d'arriver à un point de rendez-vous. Chaque instance attend de manière aléatoire avant d'invoquer la méthode proc_rdv() de l'objet RendezVous partagé.
package rdv;
class Processus extends Thread {
private RendezVous rv;
public Processus(RendezVous rv) {
this.rv = rv;
}
public void run() {
try {
Thread.sleep(6000 * (int) Math.random());
rv.proc_rdv();
} catch (InterruptedException e) {
// Gestion de l'interruption du thread
}
}
}
Classe Main
La classe Main est le point d'entrée du programme. Elle instancie un objet RendezVous avec le nombre total de processus attendus et crée ensuite ce nombre de threads Processus, les démarrant pour qu'ils participent à la barrière.
package rdv;
public class Main {
public static void main(String argv[]) {
int N = Integer.parseInt(argv[0]); // Nombre de processus
RendezVous rv = new RendezVous(N);
Processus process[] = new Processus[N];
for (int i = 0; i < N; i++) {
process[i] = new Processus(rv);
process[i].setName("Processus" + i);
process[i].start();
}
}
}
Classe RendezVous
Cette classe implémente la logique de la barrière. Elle utilise un sémaphore s pour bloquer les processus arrivés en avance et un sémaphore mutex pour garantir l'accès exclusif à la variable nbArrives. Le dernier processus à arriver libère tous les autres threads bloqués.
package rdv;
import java.util.concurrent.Semaphore;
public class RendezVous {
private int nbArrives;
private int N;
private Semaphore s; // Sémaphore pour bloquer les threads
private Semaphore mutex; // Sémaphore pour protéger l'accès à nbArrives
public RendezVous(int MaxProcess) {
nbArrives = 0;
s = new Semaphore(0, true); // Initialisé à 0, donc tout acquire bloque au début
mutex = new Semaphore(1, true); // Mutex pour garantir l'accès exclusif
N = MaxProcess;
}
public void proc_rdv() throws InterruptedException {
mutex.acquire(); // Acquiérir le mutex avant de modifier nbArrives
nbArrives = nbArrives + 1;
if (nbArrives < N) {
mutex.release(); // Libérer le mutex avant de se bloquer
System.out.println(Thread.currentThread().getName() + " se bloque ");
s.acquire(); // Se bloquer en attendant les autres processus
} else {
// Le dernier processus est arrivé
mutex.release(); // Libérer le mutex
System.out.println(Thread.currentThread().getName()
+ " est le dernier, il reveille les autres" + " processus ("+nbArrives+" sont arrivés)");
for (int i = 1; i <= N - 1; i++) // Réveiller N-1 processus bloqués
s.release(); // Libérer un permis pour chaque processus en attente
}
}
}
Problème du Producteur/Consommateur
Le problème Producteur/Consommateur est un défi classique de la programmation concurrente où des threads (producteurs) génèrent des données et d'autres threads (consommateurs) les traitent, en utilisant une ressource partagée comme un tampon. Les sémaphores sont employés pour prévenir la production sur un tampon plein et la consommation depuis un tampon vide, et pour assurer l'accès mutuellement exclusif au tampon.
Classe Item
Cette classe représente les objets ou données qui seront produits par les producteurs et consommés par les consommateurs. Elle est simple et contient une valeur de type double.
package prodConsVect;
public class Item {
public double item;
public Item(double item) {
this.item = item;
}
}
Classe Main
La classe Main configure l'environnement du problème Producteur/Consommateur. Elle initialise un Vector comme tampon partagé et trois sémaphores : empty (places disponibles), full (éléments présents) et mutex (accès au tampon).
package prodConsVect;
import java.util.Vector;
import java.util.concurrent.Semaphore;
public class Main {
public static void main(String[] args) {
Vector<Item> v = new Vector<Item>(); // Tampon partagé
Semaphore empty = new Semaphore(10); // Sémaphore pour les places vides (initialement 10)
Semaphore full = new Semaphore(0); // Sémaphore pour les éléments pleins (initialement 0)
Semaphore mutex = new Semaphore(1); // Sémaphore pour l'accès mutuellement exclusif au tampon
Consommateur cons = new Consommateur(v, full, empty, mutex);
Producteur prod = new Producteur(v, full, empty, mutex);
cons.start(); // Démarrer le consommateur
prod.start(); // Démarrer le producteur
}
}
Classe Producteur
Le thread producteur est responsable de la création d'objets Item et de leur ajout au tampon partagé. Il utilise les sémaphores pour s'assurer qu'il y a de la place disponible avant d'ajouter un élément et que l'accès au tampon est exclusif pendant l'opération.
package prodConsVect;
import java.util.Vector;
import java.util.concurrent.Semaphore;
public class Producteur extends Thread {
private Vector<Item> v;
private Semaphore full;
private Semaphore empty;
private Semaphore mutex;
public Producteur(Vector<Item> v, Semaphore full, Semaphore empty,
Semaphore mutex) {
super();
this.v = v;
this.full = full;
this.empty = empty;
this.mutex = mutex;
}
public void run() {
while (true) {
try {
empty.acquire(); // Demander une place vide (attend si le tampon est plein)
mutex.acquire(); // Acquiérir le mutex pour accéder au tampon
Item i = new Item(Math.random());
v.add(i); // Ajouter l'élément au tampon
System.out.println("Produced : " + i.item);
mutex.release(); // Libérer le mutex
full.release(); // Signaler qu'un élément est disponible
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Classe Consommateur
Le thread consommateur retire les objets Item du tampon partagé pour les traiter. Il utilise les sémaphores pour s'assurer qu'il y a des éléments à consommer avant d'en prendre un et que l'accès au tampon est exclusif pendant cette opération.
package prodConsVect;
import java.util.Vector;
import java.util.concurrent.Semaphore;
public class Consommateur extends Thread {
private Vector<Item> v;
private Semaphore full;
private Semaphore empty;
private Semaphore mutex;
public Consommateur(Vector<Item> v, Semaphore full, Semaphore empty,
Semaphore mutex) {
super();
this.v = v;
this.full = full;
this.empty = empty;
this.mutex = mutex;
}
public void run() {
while (true) {
try {
full.acquire(); // Demander un élément disponible (attend si le tampon est vide)
mutex.acquire(); // Acquiérir le mutex pour accéder au tampon
Item i = v.firstElement(); // Récupérer le premier élément
v.remove(0); // Supprimer l'élément du tampon
System.out.println("Consumed : " + i.item);
mutex.release(); // Libérer le mutex
empty.release(); // Signaler qu'une place est devenue vide
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Foire Aux Questions (FAQ)
- Qu'est-ce qu'une barrière de synchronisation ?
- Une barrière de synchronisation est un point de rendez-vous dans un programme concurrent où plusieurs threads doivent se rassembler et attendre que tous les autres threads désignés les rejoignent. Une fois que tous les threads ont atteint la barrière, ils peuvent tous continuer leur exécution simultanément.
- Comment les sémaphores sont-ils utilisés pour résoudre le problème Producteur/Consommateur ?
- Pour le problème Producteur/Consommateur, les sémaphores gèrent la disponibilité des ressources et l'accès exclusif. Un sémaphore
emptycontrôle les espaces disponibles dans le tampon (le producteur l'acquiert avant d'écrire). Un sémaphorefullcompte les éléments remplis (le consommateur l'acquiert avant de lire). Un sémaphore binairemutexassure qu'un seul thread accède au tampon partagé à la fois, évitant ainsi les corruptions de données. - Quel est le rôle du sémaphore
mutexdans ces exemples ? - Le sémaphore
mutex, initialisé à 1, agit comme un verrou d'accès exclusif. Il garantit qu'une seule thread à la fois peut exécuter une section critique du code, comme la modification d'une variable partagée (nbArrivesdans la barrière) ou l'accès à une structure de données partagée (leVectordans le producteur/consommateur). Cela prévient les conditions de concurrence et assure l'intégrité des données.