TP4 - Création de threads
Dans ce TP, on va coder un mini simulateur de voitures.
Partie 0 - Instructions
Ce TP sera noté comme suit :
- 2/10 si le programme ne compile pas.
- 4/10 si l'exercice 1 fonctionne.
- 5/10 si l'exercice 2 fonctionne aussi.
- 7/10 si l'exercice 3 fonctionne aussi.
- 8/10 si l'exercice 4 fonctionne aussi.
- +1 un point par amélioration de la partie 2.
Vérifier que son programme compile et le tester avant de l'envoyer
Partie 1. Vroum 🏎️
👉 Le fichier à rendre s'appelle votrelogin_cars.c
👈
Ce mini-jeu va fonctionner dans le terminal.
Quand on fait de bêtes printf
dans un terminal,
on ne choisit pas vraiment où la ligne s'affiche.
Elle s'affiche à la suite de la ligne précédente,
et la plupart du temps, c'est très bien comme ça.
Ici, on veut déplacer nos petites voitures sur une route qui ressemble à ça :
----------------------------------------
- - - - - - - - - - - - - - - - - - - -
----------------------------------------
On pourrait clear
le terminal à chaque tour de jeu, puis réafficher toutes
les lignes et les voitures. Mais ce n'est pas très pratique, et ça fait
clignoter le jeu, ce qui est désagréable.
On va donc faire mieux : au lieu de tout réécrire à chaque fois, on va choisir où on affiche nos caractères dans le terminal. On va donc librement se déplacer dans le terminal, au lieu de réafficher tout le jeu à chaque tour.
C'est ce mode d'affichage plus "libre" que nous allons découvrir qui est utilisé par les Text User Interfaces, applications interatives de terminal, comme
less
,vim
,nano
outop
.
Une librairie C facilitant (un peu) l'utilisation du terminal existe :
ncurses
. On ne l'utilisera pas ici, on va voir qu'il suffit de quelques caractères spéciaux pour faire ce que l'on veut.
Voici le départ de votre fichier votrelogin_cars.c
:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
game_init();
sleep(3);
game_close();
}
Exercice 1 - Initialisation du jeu
Le but de cet exercice est d'écrire deux fonctions :
void game_init();
void game_close();
Elles permettent de lancer et arrêter le jeu.
stdin
etstdout
.Ces deux variables déclarées dans
<stdio.h>
sont desFILE *
qui représentent les entrée et sortie standards. On va les utiliser tout au long du TP dans nos appels àfprintf()
etfgetc()
game_init
va commencer par effacer l'écran. Pour cela, il faut envoyer deux escape sequences, séquences d'échappement, au terminal :
\e[1;1H
, qui déplace le curseur ligne 1, colonne 1.\e[2J
, qui efface tout ce qui se trouve après le curseur.
Solution
On peut par exemple définir et utiliser cette fonction :
void clear_screen() {
fprintf(stdout, "\e[1;1H\e[2J");
}
game_init
va ensuite déplacer le curseur en ligne 1, colonne 1, pour afficher la première partie de la route. Pour cela, on va utiliser la bonne séquence d'échappement. Voici un exemple, qui amène en ligne 1, colonne 2 :\e[1;2H
. Écrire la fonction suivante, qui envoie la bonne séquence d'échappement, et l'utiliser dansgame_init
.
void move_cursor(unsigned int y, unsigned int x);
-
game_init
doit ensuite afficher 40-
, puis sauter deux lignes plus bas, etc, jusqu'à afficher toute la route, comme indiqué au début de la partie 1. -
Enfin,
game_init
doit s'assurer que tous les caractères envoyés àstdout
ont bien été envoyés au terminal, en utilisant la fonction standardint fflush(FILE *file);
. -
game_close
se contentera d'effacer l'écran et de fflush.
Compiler et exécuter le programme. On devrait voir la route s'afficher trois secondes, puis l'écran s'effacer et le programme quitter.
Exercice 2 - Lancement du premier bolide
Le lancement d'une voiture va se faire dans une fonction, qu'on réécrira au prochain exercice pour qu'elle s'exécute dans un nouveau thread.
Voilà sa définition :
void car_thread(int route);
route
peut prendre deux valeurs :
0
: route du haut, on va de droite à gauche1
: route du bas, on va de gauche à droite
- En fonction du paramètre route, la fonction détermine :
- quel caractère elle va afficher pour la voiture (par exemple
<
pour une voiture qui va vers la gauche et>
pour une voiture qui va vers la droite). - de quel côté de la route elle part.
- de quelle route elle part.
- La largeur de la route est de 40 caractères, donc la fonction va itérer 40 fois.
À chaque tour de boucle :
-
On remet le curseur à la position actuelle et on affiche un espace, pour effacer l'ancien
>
/<
. -
On se décale sur la gauche ou sur la droite et on affiche le nouveau
>
/<
. -
On décale le curseur en ligne 1, colonne 41 (on verra plus tard pourquoi).
-
On fflush.
-
On dort un nombre aléatoire entre 50 et 60 millisecondes. On aura besoin de
usleep
etrand
.
À la fin de la boucle :
- On répète les étapes 3 et 6.
Supprimer le sleep
dans votre fonction main et mettre deux appels de la
fonction car_thread
, avec 0 et 1 comme arguments respectifs.
Compiler et exécuter le programme.
❓ Les voitures s'affichent-elles en même temps, ou chacune leur tour ?
Exercice 3 - Threads !
On veut maintenant pouvoir lancer plusieurs voitures en même temps.
La fonction car_thread
doit maintenant avoir cette définition :
void *car_thread(void *param);
-
La fonction reçoit maintenant un pointeur sur
void
. Mais qu'est-ce que ça veut dire Jamy ?. Et bien, ça veut dire que c'est un pointeur sur quelque chose dont C ne connait pas le type à l'avance. On va donc commencer par déclarer une variableint
qui va stocker le paramètreroute
. On veut convertirparam
enint *
, puis récupérer sa valeur pointée et la stocker dansroute
. -
car_thread
doit également retournerNULL
à la fin pour être correcte.
Et c'est bon, notre fonction est prête à être utilisée pour lancer un thread
avec la fonction pthread_create
.
Voici sa définition :
int pthread_create(
/* l'adresse d'une variable pthread_t, pour stocker son thread id */
pthread_t *thread_id,
/* paramètres supplémentaires de la librairie pthread, on mettra NULL */
pthread_attr_t attr,
/* nom d'une fonction qui prend en paramètre un void* et qui retourne un void* */
void *(*function)(void *),
/* argument à envoyer à la fonction */
void *arg);
- Remplacer les deux appels directs à
car_thread
dans le main par des appels àpthread_create
pour lancercar_thread
. Attendre 10 millisecondes entre les deux appels.
Compiler et exécuter le programme. Qu'est-ce qu'il se passe ?
Réponse
Le programme n'attend pas la fin de l'exécution des threads pour quitter !
Mais comment va-t-on faire ? 🙀
-
Changer le deuxième appel à
pthread_create
pour stocker le thread id de la deuxième voiture. -
Attendre la fin de son exécution pour quitter. On utilisera la fonction suivante pour attendre. On peut mettre
NULL
comme 2e argument, puisqu'on s'en fiche de ce que retournecar_thread
.
int pthread_join(
/* le thread qu'on veut attendre */
pthread_t thread_id,
/* un pointeur pour récupérer le retour de la fonction-thread */
void **return_value);
- Compiler et exécuter le programme. Vérifier que les deux voitures roulent simultanément, et que le programme quitte quand elles ont fini de rouler.
Il y a une segfault ? C'est sûrement l'étape 1 qui pose problème. Voici sa solution :
void *car_thread(void *param) {
int route = *( (int *) param);
...
On doit aussi appeler pthread_create
avec l'adresse d'un int
comme argument, sinon ça ne marchera pas !
Par exemple :
int arg = 0;
pthread_create(..., ..., ..., &arg);
Exercice 4 - Entrées utilisateur
Notre programme serait un peu plus fun si l'utilisateur pouvait taper sur des touches pour lancer des voitures, et quitter en appuyant sur q. C'est le but de l'exercice.
-
On va donc faire un
do { ... } while(...);
dans notremain
. -
Dedans, on va récupérer un caractère de stdin avec
input = fgetc(stdin);
-
Si la touche est
j
, on lance une voiture sur la route du bas. -
Si la touche est
k
, on lance une voiture sur la route du haut. -
Si la touche est
q
, on sort de la boucle. -
À la fin, on attend que la dernière voiture ait fini de traverser avant d'appeler
game_close
. Il nous faut donc une variable qui stocke le thread id de la dernière voiture lancée.
Partie 2. Améliorations
- Chaque amélioration rapporte un point supplémentaire.
- Elles peuvent être faites dans n'importe quel ordre.
- Si la note dépasse 10/10, les points supplémentaires compenseront d'autres notes de TP.
Compteur
Afficher à côté de la route un compteur du nombre total de voitures passées sur la route.
Le main mettra à jour une variable globale à chaque nouvelle voiture. Il lancera au début du programme un thread, qui mettra à jour l'affichage du compteur grâce à la variable globale toutes les secondes.
Couleur
Faire en sorte que les voitures s'affichent d'une couleur aléatoire.
On pourra s'inspirer de loremipsum
, du TP3
Input
- Faire en sorte qu'il n'y ait pas besoin d'appuyer sur entrée pour que la touche pressée soit prise en compte.
- Faire en sorte que ce que tape l'utilisateur ne se voie pas à l'écran.
Chercher sur Internet les fonctions C qui permettent de changer le mode du
terminal. Attention, on veut que le programme remette l'ancien mode en
quittant, sinon votre terminal sera buggé. On peut débugger un terminal en
lançant la commande reset
.
Taille
Demander à l'utilisateur la taille de la route avant de démarrer le simulateur.
Messages
- Afficher des messages sous la route pour indiquer quelle touche fait quoi.
- Afficher en-dessous un message d'erreur pendant 2 secondes si une mauvaise touche est pressée.
- Afficher un message quand la touche q est tapée, indiquant qu'on s'apprête à quitter.