Département INFormatique 
  CSC4508/M2 : Concepts des systèmes d'exploitation et mise en œuvre sous Unix
    Contenu

Threads

(Corrigés)
Variante de l'exercice fil rouge du TP du chapitre Communication inter-processus: gestion des impressions.

Exercice : gestion des impressions

1. Un thread par impression

On reprend le programme imprime0.c évoqué dans les exercices sur les communications et synchronisations entre processus. On souhaite modifier ce programme pour que le thread principal crée, chaque impression, un thread chargé de l'impression. Vérifier que:

  • plusieurs threads peuvent s'exécuter en même temps;
  • chaque thread imprime un fichier différent du thread voisin.
  • Remarque: pour simplifier, il est possible de limiter le nombre total d'impressions.

Visualisez les différents threads avec la commande ps (pour afficher les différents threads: ps -m ou ps -L selon les versions).

---beginCorr
imprime_thread1.corrige.c

Les pièges à éviter

1. Partage de la zone nomFic

Dans le programme principal, la zone pointée par la variable locale nomFic est utilisée par tous les threads pour écrire le nom du fichier à imprimer. Donc un thread peut être en cours d'impression pendant qu'un autre thread écrit un nouveau nom de fichier à imprimer. Le thread précédent terminera alors son impression en prenant en compte le nouveau nom!

Solution: définir une zone propre à chaque thread.

Il y a plusieurs façons de le faire. La première solution consiste à définir un tableau alloué statiquement char nomFic[MAXNBTHREADS][MAXSIZE];, le thread i utilisant la zone nomFic[i]. Une deuxième solution consiste à allouer dynamiquement dans la boucle la zone nomFic: nomFic = malloc(MAXSIZE * sizeof(char));. Chaque thread utilise alors sa propre zone mais attention il faudra aussi qu'il la libère!

2. Sérialisation des impressions

Si la création et la terminaison se font séquentiellement dans la même boucle, comme ci-dessous, alors les threads seront sérialisés, c'est-à-dire que la création d'un thread ne s'effectue qu'après la fin de l'exécution et la terminaison du thread précédent. En effet pthread_join() est bloquant et attend la fin du thread précédent.

while () {
pthread_create(&thread, ....., func, nomFic);
pthread_join(thread,NULL);
}
Pour faire en sorte que plusieurs threads puissent s'exécuter en même temps, il faut scinder la boucle en deux: une boucle de création (avec compteur de threads créés) et une boucle de terminaison:
while () {
pthread_create(&thread[i], ....., func, nomFic);
nbThreads++;
}
for (i = 0; i < nbThreads; i++) {
pthread_join(thread[i],NULL);
}

Utilisation de threads détachés

while () {
pthread_create(&thread, ....., func, nomFic);
pthread_detach(thread);
}

Dans le cas de threads détachés, le thread principal ne peut pas attendre la terminaison des threads pairs. Il n'y a donc pas besoin de gérer un tableau de threads.

Le thread principal peut se terminer avant les autres threads donc ne pas appeler exit parce qu'exit termine le processus (donc tous les threads).

Si nomFic est un tableau statique, il ne faut pas l'allouer dans la pile du thread principal parce que le thread principal peut se terminer et libérer ses ressources avant la fin des autres threads. nomFic doit être en variable globale.

---endCorr

2. Synchronisation autour de l'imprimante

Reprendre le programme précédent pour que les threads ne mélangent pas leurs impressions à l'imprimante.

Question (de réflexion) 2.1: synchronisation

Quel est le type de synchronisation à mettre en place entre les threads chargés de l'impression?

Question 2.2: implantation

Implanter avec un mutex de la bibliothèque POSIX thread.

---beginCorr
imprime_thread2.corrige.c
---endCorr

3. Utilisation d'une zone tampon circulaire

On ne crée plus un thread à chaque demande d'impressions. On crée au préalable un ensemble de threads de taille MAXNBTHREADS = 3 threads. Chaque thread se met en attente d'une impression, imprime puis se met en attente d'une nouvelle impression. De plus, une zone tampon circulaire contenant de taille MAXNBIMPRESSIONS = 5 est utilisée pour stocker la file d'impressions. L'accès à l'imprimante reste exclusif.

Question (de réflexion) 3.1: synchronisation

Quel est le type de synchronisation à mettre en place entre le thread principal et les autres threads?

Écrire l'algorithme sur papier.

Question 3.2: implantation avec sémaphores

Écrire ce programme en utilisant des sémaphores POSIX.

---beginCorr
imprime_thread3_1.corrige.c

Synchronisation

Paradigme producteur/consommateur: 1 producteur, le thread principal, transmet les impressions aux consommateurs, les threads pairs avec tampons circulaires.

---endCorr

Question 3.3: implantation avec conditions

Écrire ce programme en utilisant des conditions.

---beginCorr
imprime_thread3_2.corrige.c
---endCorr

4. Terminaison de l'application

Le thread producteur s'arrête lorsque l'utilisateur tape 0. Les threads consommateurs doivent s'arrêter lorsqu'ils ont terminé leurs impressions et que l'utilisateur a signifié au thread producteur la terminaison de l'application. Proposez deux versions de la terminaison:

  1. la première version doit faire en sorte qu'à la fin de l'application, chaque thread consommateur appelle pthread_exit();
  2. la seconde version doit utiliser le fait qu'un thread peut annuler un autre thread.
---beginCorr
imprime_thread4_version1.corrige.c
imprime_thread4_version2.corrige.c
---endCorr

Pour aller plus loin

Variante de l'exercice fil rouge du TP du chapitre Client-Serveur: MetMme.

Exercice M. et Mme...

Dans cet exercice, on se propose d'écrire un serveur de "M. et Mme" :
  • Le client lit, sur sa ligne de commande, le nom de famille de "M. et Mme". Il l'envoie au serveur.
  • Le serveur cherche ce nom de famille dans le fichier mEtMme.txt :
    • s'il le trouve, il renvoie le nom du fils, de la fille ou bien des enfants
    • sinon il renvoie le message "Désolé, je ne le connais pas"
  • Le client affiche le message reçu du serveur.

Architecture client/serveur

  1. Le client ouvre (au sens mkfifo du terme) un tube nommé dont le nom est fonction de son "pid" (de manière à ne pas interférer avec d'autres clients potentiels).
  2. Le client se "connecte" au serveur via un autre tube nommé (défini par le serveur).
  3. La requête du client contient non seulement le "M. et Mme" cherché, mais aussi le nom du tube nommé qui a été créé en 1. par le client et sur lequel le client attend la réponse du serveur.

Attention: repartir des corrigés du chapitre client-serveur:

Question 1: création de threads à la volée

Programmer le serveur de sorte que le thread principal crée un thread à chaque fois qu'il reçoit une requête. Le nouveau thread ouvre le fichier "M. et Mme" et traite la requête:

  • il cherche le "M. et Mme" dans le fichier ;
  • et il répond au client.

---beginCorr
mEtMmeServeur_threadQ1.corrige.c

Les pièges à éviter

  • Il faut utiliser une requête différente pour chaque thread
  • Ne pas allouer les requêtes dans la pile du thread principal en cas de threads détachés
  • Ne pas sérialiser l'exécution des threads
---endCorr

Question 2: création au préalable d'un ensemble de threads et utilisation d'une zone tampon circulaire

Programmer le serveur de sorte que le thread principal crée un pool de N threads:

  • chaque thread ouvre le fichier et attend une requête du thread distributeur (le thread principal);
  • sur réception d'une requête, le thread distributeur fait suivre la requête à un thread en attente;
  • le thread traite la requête (voir question précédente); et une fois terminée, attend une autre requête du thread distributeur.

---beginCorr
mEtMmeServeur_threadQ2.corrige.c

Synchronisation

Paradigme producteur/consommateur: 1 producteur, le thread principal, transmet les requêtes clientes aux consommateurs, les threads pairs avec tampons circulaires.

---endCorr

Last modified: Tue April 07 10:45:34 CEST 2015