CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
3IF1020 - Programmation système - TP n°2

Table des matières

  • Date de la séance de TP : mardi 29 octobre 2024 - 13h45
  • Date limite de prise en compte de votre version sur votre dépôt GitLab : dimanche 10 novembre 2024 - 23h59

Utiliser les fichiers du dossier ps_tp2 à la racine de votre dépôt GitLab, seul le contenu de ce dossier sera pris en compte pour l'évaluation de votre travail sur ce TP.

Les noms des fichiers à utiliser sont indiqués dans ce sujet.


Les programmes seront écrits en C++.

La version avec processus légers n'utilise que du C++ standard (version 2011 au moins).

La version avec processus séparés utilise une bibliothèque de boost, des indications pour l'installer (déjà fait sur MyDocker) vous sont données.


1. Thème

Un problème classique de synchronisation de threads ou processus est connu sous le nom « Problème des producteurs et des consommateurs » : les producteurs fournissent des données aux consommateurs, cet échange se fait par l'intermédiaire d'une « boîte à lettres » qui est gérée en FIFO et a la particularité d'avoir une taille limitée. Il faut donc faire en sorte qu'un producteur soit suspendu quand la boîte est pleine, et qu'un consommateur le soit quand la boîte est vide.

Le type des données échangées est, bien sûr, sans importance pour les aspects liés à la synchronisation, nous utiliserons ici des entiers. Par ailleurs, les aspects algorithmiques de gestion de la boîte aux lettres (gérée comme un tableau circulaire) ne sont pas l'objet de cet exercice, le code vous est donc donné.

Dans une première version, les producteurs et consommateurs seront des processus légers (ou thread). La seconde version utilisera des processus et la mémoire partagée.

2. Description des fichiers et du code fourni

2.1. Générateur de nombres aléatoires

  • Fichier : Random.hpp

Cette classe fournit un service de génération de nombres aléatoires entre 0 et une valeur maximale précisée à la construction. Son utilisation est très simple :

#include "Random.hpp"

// Création du générateur pour des entiers entre 0 et 50
Random generator{ 50 };
// Obtention d'un nombre aléatoire
int alea{ generator() };

La principale utilisation de ce service est de mettre en sommeil un thread pendant un temps aléatoire, ce qui peut être fait ainsi :

#include <chrono>
#include <thread>

// Définition de l'unité de temps : un nombre entier de microsecondes
using microseconds = std::chrono::duration< int, std::micro >;

// Dans une méthode ou une fonction exécutée par un thread :
{
    // Attente aléatoire entre 0 et 50 µs
    std::this_thread::sleep_for( microseconds{ generator() });
}

2.2. Flux de sortie synchronisé

  • Fichier : thread/osyncstream.hpp

Quand plusieurs threads veulent afficher des messages dans un même terminal, il faut mettre en place un mécanisme pour éviter que les messages se mélangent.

C++20 propose pour cela osyncstream qui s'utilise ainsi :

// Dans une méthode ou une fonction exécutée par un thread :
{
    // L'affichage n'est réellement effectué que dans le destructeur de osyncstream 
    // qui est automatiquement appelé à la fin de l'instruction.
    osyncstream{ std::cout } << "hello" << " " <<  "world" << "\n";
}

Le fichier osyncstream.hpp propose ce service sous une forme très limitée (pas de manipulateur comme std::endl par exemple), mais suffisante pour ce TP (peu d'environnements avec une bibliothèque standard C++20).

Pour en comprendre l'utilité, vous pouvez commencer par faire, dans les fichiers thread/Producer.hpp et thread/Consumer.hpp, vos affichages sur std::cout.


2.3. Boîte à lettres

  • Fichiers : BasicMessageBox.hpp, thread/MessageBox.hpp et process/MessageBox.hpp

La classe BasicMessageBox offre le service algorithmique (sans synchronisation) de boîte à lettres. Pour détecter les erreurs, les places vides de la boîte ont leur valeur fixée à -1. Cette particularité ne doit pas être utilisée pour savoir si une place est vide ou pas dans la boîte.

Il faudra ajouter à ce service les mécanismes de synchronisation entre threads (fichier thread/MessageBox.hpp) ou entre processus (fichier process/MessageBox.hpp).

2.4. Producteur et consommateur

  • Fichiers : ProdOrCons.hpp, thread/Producer.hpp, thread/Consumer.hpp, process/Producer.cpp et process/Consumer.cpp

La classe ProdOrCons est la superclasse des producteurs et consommateurs, elle stocke les attributs nécessaires, fournit le constructeur, et définit sous forme abstraite l'opérateur appel de fonction qui sera utilisé pour lancer l'exécution du producteur ou du consommateur.

Les comportements du producteur et du consommateur doivent être écrits dans les fichiers thread/Producer.hpp et thread/Consumer.hpp pour la version avec threads, dans les fichiers process/Producer.cpp et process/Consumer.cpp pour la version avec processus.

Les fichiers process/Producer.cpp et process/Consumer.cpp définissent aussi la fonction principale du producteur et du consommateur.

3. Un producteur et un consommateur en processus légers (1,5 point)

  • Compléter le code du producteur (fichier thread/Producer.hpp) et du consommateur (fichier thread/Consumer.hpp).

Vous devez afficher des messages pour suivre l'avancement dans le code du producteur et du consommateur (mais pas dans le code de la boîte à lettres).


  • Écrire la fonction one_producer_one_consumer() (fichier thread/Threads.cpp) qui crée les différents objets et threads ; le producteur déposera 20 messages dans la boîte, le consommateur en extraira autant.
  • Compiler et exécuter le programme, constater que le comportement n'est pas satisfaisant (extraction par le consommateur de nombres négatifs).

add, commit & push


  • Ajouter dans thread/MessageBox.hpp les instructions nécessaires de synchronisation afin d'obtenir un comportement correct. Vous aurez besoin d'utiliser des mutex, des unique_lock et des condition_variable.

add, commit & push


4. Des producteurs et des consommateurs en processus légers (1 point)

  • Écrire la fonction several_producers_and_consumers() qui crée un nombre aléatoire, entre 10 et 20, de consommateurs (chacun extraira 20 messages), 2 fois plus de producteurs (chacun déposera 10 messages) ; la même boîte à lettres est utilisée. Vérifier le bon fonctionnement, corriger si nécessaire.

Il est possible de mémoriser les threads dans un tableau ainsi :

    std::vector< std::thread > group;
    group.push_back( std::thread{ Producer{ ... )}};

Selon la façon dont votre code de synchronisation a été écrit dans thread/MessageBox.hpp, il est possible que vous observiez un dysfonctionnement avec plusieurs producteurs/consommateurs alors que tout semble fonctionner correctement avec un seul producteur et un seul consommateur.

La situation suivante, par exemple côté consommateur, peut se produire :

  • un consommateur trouve la boîte vide et se met en attente sur la variable de condition (état bloqué) ;
  • un producteur dépose un message et signale que la boîte n'est pas vide, le consommateur qui était dans l'état bloqué passe dans l'état prêt ;
  • avant que ce dernier ne devienne actif, un autre consommateur est activé et va prendre la donnée déposée par le producteur ;
  • quand le consommateur qui était bloqué devient enfin actif, la boîte est de nouveau vide !

Vous devriez en déduire les corrections à apporter à votre code si nécessaire.


add, commit & push


5. Partie avancée : un producteur en un consommateur en processus séparés (1,5 point)

L'objectif est maintenant que le producteur et le consommateur soient des processus différents qui utiliseront une boîte à lettres située dans une zone de mémoire partagée. Vous pouvez reprendre le code précédent du producteur et du consommateur, en supprimant les parties non utiles (par exemple, comme il n'y aura qu'un seul processus léger, le processus lui-même, il est inutile de conserver le mécanisme de sérialisation de l'affichage).

Vous ne devez plus, dans cette partie, utiliser les mécanismes de synchronisation entre threads qui sont inopérants entre processes. Il reste par contre possible d'utiliser, par exemple, un sleep_for().


Les systèmes d'exploitation fournissent en général des services de communication et d'accès à des zones de mémoire partagée, des mécanismes de synchronisation entre processus... Cependant, il peut y avoir des différences plus ou moins grandes selon les différents systèmes (en particulier entre Windows® et Unix®) et les API ne sont pas les mêmes.

Une solution courante à ce problème est d'utiliser une bibliothèque qui va masquer ces différences et proposer une interface unique. Nous utiliserons Boost.Interprocess pour cet exercice.

Boost est installée sur l'environnement MyDocker.


Vous trouverez en particulier sur la page de documentation sur la mémoire partagée des exemples de code que vous pourrez en partie reprendre pour cet exercice.

La majorité des bibliothèques boost sont headers only, c'est le cas de la partie dont nous avons besoin.

Pour installer uniquement cette partie (commandes en mode terminal) :

 wget https://wdi.centralesupelec.fr/3IF1020/downloads/Main/boost-interprocess.zip
 unzip boost-interprocess.zip

Il existe aussi une version tar compressée avec bzip2 pour celles et ceux qui savent comment utiliser un tel fichier (taille environ 2 fois plus petite que la version zip).

Pour l'utiliser avec un code source programme.cpp (librt est nécessaire sur repli.it, pas forcément avec les autres environnements) :

 c++ -std=c++20 -I boost-interprocess programme.cpp -lpthread -lrt

Pour installer complètement boost :

  • Sur Windows, les indications sont décrites ici.
  • Sur MacOS, le plus simple est d'installer brew si vous ne l'avez pas encore fait, puis d'installer boost avec la commande brew install boost.
  • Sur Linux, les distibutions classiques proposent boost et une commande sudo apt-get install libboost-all-dev devrait faire le nécessaire.

Comment faire pour placer votre boîte à l'endroit où est la zone de mémoire partagée ?

Cela s'appelle un placement new : si on veut imposer qu'une instance de MessageBox soit placée à une adresse addr donnée, on écrit :

ProcessMessageBox * box{ new( addr ) MessageBox( /* arguments du constructeur */ )};

La boîte à lettres ne doit être initialisée qu'une seule fois via ce placement new, par exemple par le producteur ; le consommateur utilisera simplement un static_cast pour considérer que l'adresse du bloc de mémoire partagé est en fait l'adresse de la boîte à lettres.


Comment faire pour que le consommateur attende que l'initialisation de la boîte à lettres soit faite par le producteur ?

Vous pouvez utiliser un sémaphore nommé.



add, commit & push