CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Génie logiciel orienté objet - Exercice : threads et synchronisation

Exercice

TODO:

  • Pour lecteur/rédacteur, la détection du premier/dernier est pertinent si on utilise des sémaphores, pas si on utilise des moniteurs
  • afficher messages d'erreurs si protocole non respecté

1. Création d'un thread – synchronisation sur la fin du thread

  • Lancer Eclipse et créer un projet Java gloo.synchronisation.
  • Créer une classe EssaiThread (qui contiendra une fonction main()) qui étend la classe java.lang.Thread et dont le constructeur reçoit un nom qu'il transmet au constructeur de la super-classe.
  • Surcharger la méthode run() pour :
    • afficher un premier message, préfixé de son nom, indiquant le lancement ;
    • attendre un temps aléatoire entre 0 et 1 seconde (voir Thread.sleep(long)) ;
    • afficher un second message, lui aussi préfixé de son nom, indiquant la terminaison.
  • Dans la fonction main() de cette classe, en affichant un message avant chaque étape et après la dernière :
    • créer une instance de votre classe ;
    • lancer son exécution comme thread (voir Thread.start()) ;
    • attendre la fin de cette exécution (voir Thread.join()).
  • Vérifier le bon fonctionnement.

2. Création d'un thread – synchronisation par moniteur

  • Créer une classe EssaiRunnable :
    • qui implémente l'interface java.lang.Runnable ;
    • qui possède un attribut static de type Object qui sera utilisé comme moniteur ;
    • qui a un constructeur prenant comme argument un nom et le mémorise en attribut ;
    • dont la fonction run() :
      • affiche un premier message, préfixé de son nom, indiquant son lancement ;
      • attend un temps aléatoire entre 0 et 1 seconde ;
      • affiche un second message indiquant qu'il va réveiller le thread principal ;
      • réveille via le moniteur (en utilisant Object.notify()) le thread principal ;
      • affiche un dernier message indiquant sa terminaison.
  • Dans la fonction main() de cette classe, en affichant un message avant chaque étape et après la dernière :
    • créer l'objet qui servira de moniteur ;
    • créer une instance de votre classe ;
    • lancer son exécution comme thread ;
    • attendre le signal de sa fin sur le moniteur (en utilisant Object.wait()).
  • Vérifier le bon fonctionnement.

3. Un producteur et un consommateur sans synchronisation

Le problème producteurs-consommateurs est un classique dans le domaine de la synchronisation de processus (ou threads).

Sa description générique est la suivante :

  • un ou des producteurs produisent des données à destination d'un ou plusieurs consommateurs (le type de ces données est ici sans importance) ;
  • pour transférer les données d'un producteur vers un consommateur, un objet intermédiaire, appelé classiquement boîte à lettres, est utilisé ;
  • cette boîte a une capacité limitée, il faut donc gérer les problèmes de boîte pleine et de boîte vide, et éviter qu'un producteur écrase la donnée d'un autre ou que plusieurs consommateurs récupèrent la même donnée.

Nous allons examiner dans la suite différentes solutions à ce problème.

  • Définir une classe BoiteALettres. Celle-ci servira d'intermédiaire entre un producteur de messages (ceux-ci seront des entiers) et un consommateur de ces messages.

Cette boîte à lettres utilisera un tableau de taille entiers (valeur donnée au constructeur) pour stocker les messages reçus du producteur et non encore remis au consommateur. Ce tableau sera géré de manière circulaire (on recommence au début du tableau quand on est arrivé à la fin) et utilisera deux indices indiceDepot et indiceRetrait pour mémoriser la prochaine case où un message doit être déposé (respectivement : doit être extrait).

  • Définir les attributs correspondants à cette description, les initialiser dans le constructeur.
  • Définir les deux méthodes void depot(int) et int retrait() offrant les services demandés ; la méthode int retrait() mettra 0 à la place du message retiré, cela facilitera la détection des dysfonctionnements.
  • Créer une classe Producteur qui étend la classe Thread. Son constructeur recevra en argument une boite et le nombre de messages à y déposer. Sa méthode run() déposera (et affichera avant) dans la boite le nombre de messages prévu (des nombres entiers consécutifs à partir de 1) en attendant un temps aléatoire avant chaque dépôt.
  • Créer une classe Consommateur qui étend la classe Thread. Son constructeur recevra en argument une boite et le nombre de messages à extraire. Sa méthode run() retirera (et affichera) de la boite le nombre de messages prévu en attendant un temps aléatoire avant chaque retrait. Un message d'erreur sera affiché si un message extrait n'est pas strictement positif.
  • Créer une classe Main dont la méthode main() crée la boîte (de taille TAILLE_BOITE), le producteur et le consommateur, lance ces deux derniers et attend leur fin. Le nombre de messages sera fixé par une constante NOMBRE_MESSAGES.
  • Fixer les deux constantes TAILLE_BOITE et NOMBRE_MESSAGES à 10, exécuter le programme. Que constatez-vous ?

4. Un producteur et un consommateur synchronisés par moniteur

  • Définir une classe BoiteALettresAvecMoniteur, sous classe de BoiteALettres. Surcharger les méthodes void depot(int) et int retrait() pour que les dépôts et les retraits soient en exclusion mutuelle via le moniteur associé à la boîte. Le consommateur devra être suspendu s'il n y a pas de nouveaux messages pour lui.

Les méthodes de la classe BoiteALettresAvecMoniteur doivent appeler celles de BoiteALettres en ajoutant simplement la synchronisation nécessaire pour obtenir un fonctionnement correct.

Concrètement, le tableau et les indices utilisés par BoiteALettres doivent rester privés.

  • Exécuter le programme, le résultat est-il satisfaisant ?
  • Fixer la constante TAILLE_BOITE à 2, exécuter le programme, le résultat est-il satisfaisant ? Modifier votre code pour obtenir un résultat correct.

5. Deux producteurs et un consommateur synchronisés par moniteur

  • Créer dans la méthode main() un second producteur utilisant la même boîte à lettres. Le consommateur devra donc extraire deux fois plus de messages.
  • Exécuter votre code. Selon la façon dont vous l'aurez écrit, vous obtiendrez un comportement satisfaisant ou pas. Si nécessaire, corriger votre code.

6. Deux producteur et un consommateur synchronisés par sémaphore

  • Définir une classe BoiteALettresAvecSemaphore, sous classe de BoiteALettres. Surcharger les méthodes void depot(int) et int retrait() pour synchroniser, en utilisant des sémaphores, les dépôts et les retraits.

Un sémaphore gère un nombre fixe de ressources. Quelles sont les ressources nécessaires au producteur ? Au consommateur ? Donc, combien de sémaphores ? avec quelle valeur initiale du compteur ?

  • Modifier la méthode main() pour utiliser cette classe.

Votre programme va certainement montrer un fonctionnement satisfaisant si vous avez correctement utilisé les sémaphores ; pourtant, votre programme n'est pas correct. Pour essayer de mettre en évidence ce dysfonctionnement :

  • agrandir votre taille de boîte (la constante TAILLE_BOITE) pour que tous les messages déposés par les producteurs puissent être stockés dans la boîte ;
  • supprimer le temps d'attente aléatoire entre chaque message déposé par les producteurs ;
  • ajouter une attente de 0,5 seconde entre les deux instructions de la méthode BoiteALettres.depot(int).
  • Observer de nouveau les messages récupérés par le consommateur :
    • sont-ils corrects ?
    • pouvez-vous identifier la cause du problème ?
    • pourquoi ce problème est-il présent avec les sémaphores mais pas avec les moniteurs ?
    • quelle solution proposez-vous pour résoudre ce problème ?

7. Lecteurs et rédacteurs

Le problème lecteurs-rédacteurs est aussi un classique dans le domaine de la synchronisation de processus (ou threads).

Sa description générique est la suivante :

  • une information peut-être lue simultanément par beaucoup de lecteurs
  • par contre, pour la mettre à jour, il faut imposer qu'il n'y ait qu'un seul rédacteur sans aucun lecteur.

Nous modéliserons ce problème de la façon suivante :

  • une classe Information représentera la donnée accédé en lecture (par des lecteurs) ou en écriture (par des rédacteurs) ;
  • cette classe offrira les services suivants :
    • void accesLecture() utilisée par un lecteur voulant accéder à l'information ;
      • soit cette dernière est disponible (0 ou des lecteurs sont présents, mais pas de rédacteurs), l'accès est alors accordé ;
      • soit un rédacteur est présent : le lecteur est suspendu jusqu'à libération de l'information par le rédacteur ;
    • void finLecture() utilisée par un lecteur signalant qu'il n'a plus besoin d'accéder à l'information ;
    • void accesEcriture() utilisée par un rédacteur voulant modifier l'information ;
      • soit cette dernière est disponible (pas de lecteurs, pas de rédacteurs), l'accès est alors accordé ;
      • soit un lecteur ou un rédacteur est présent : le rédacteur est suspendu jusqu'à rendre possible l'accès ;
    • void finEcriture() utilisée par un rédacteur signalant qu'il a terminé la modification de l'information ;

Éléments fournis :

  • Classe Information, ses méthodes se contentent d'afficher des messages ;
  • Classes Lecteur et Redacteur, sous classes de Thread ; les constructeurs reçoivent une instance d'Information, leur méthode run() demande un accès en lecture ou en écriture selon le cas, attend un temps aléatoire puis signale la fin de l'accès ; des messages sont affichés pour signaler ces étapes ;
  • Classes FabriqueLecteurs et FabriqueRedacteurs, sous classes de Thread ; leur méthode run() est en charge de créer les lecteurs (respectivement : les rédacteurs) avec une attente de temps aléatoire entre chaque ; une fois créé le nombre d'instances prévu, les fabriques attendent que tous les lecteurs (respectivement : rédacteurs) soient terminés ;
  • Classe principale dont la méthode main() crée les fabriques et attend leur terminaison ; les paramètres numériques de simulation y sont définis sous forme de constantes.

Le projet Eclipse contenant l'ensemble de ces classes est disponible via ce lien ; il ne reste qu'à compléter la classe Information.

Une fois constaté le dysfonctionnement du système (par exemple : les rédacteurs accèdent à l'information alors que des lecteurs sont présents...), proposer une solution de synchronisation utilisant les moniteurs ou les sémaphores (il est préférable de ne pas mélanger les deux).

L'information est une ressource critique qui doit être réservée par le premier lecteur et libérée par le dernier, ou réservée par un unique rédacteur et libérée par lui. Il semble donc nécessaire de compter les lecteurs pour identifier le premier et le dernier.



© 2022-23 CentraleSupélec