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:
- la première version doit faire en sorte qu'à la fin de l'application, chaque thread consommateur appelle
pthread_exit() ;
- 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
- 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).
- Le client se "connecte" au serveur via un autre tube
nommé (défini par le serveur).
- 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
|