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 :

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 ou top.

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 et stdout.

Ces deux variables déclarées dans <stdio.h> sont des FILE * qui représentent les entrée et sortie standards. On va les utiliser tout au long du TP dans nos appels à fprintf() et fgetc()

  1. game_init va commencer par effacer l'écran. Pour cela, il faut envoyer deux escape sequences, séquences d'échappement, au terminal :
Solution

On peut par exemple définir et utiliser cette fonction :

void clear_screen() {
    fprintf(stdout, "\e[1;1H\e[2J");
}
  1. 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 dans game_init.
void move_cursor(unsigned int y, unsigned int x);
  1. 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.

  2. 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 standard int fflush(FILE *file);.

  3. 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 :

  1. En fonction du paramètre route, la fonction détermine :
  1. La largeur de la route est de 40 caractères, donc la fonction va itérer 40 fois.

À chaque tour de boucle :

  1. On remet le curseur à la position actuelle et on affiche un espace, pour effacer l'ancien >/<.

  2. On se décale sur la gauche ou sur la droite et on affiche le nouveau >/<.

  3. On décale le curseur en ligne 1, colonne 41 (on verra plus tard pourquoi).

  4. On fflush.

  5. On dort un nombre aléatoire entre 50 et 60 millisecondes. On aura besoin de usleep et rand.

À la fin de la boucle :

  1. 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);
  1. 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 variable int qui va stocker le paramètre route. On veut convertir param en int *, puis récupérer sa valeur pointée et la stocker dans route.

  2. car_thread doit également retourner NULL à 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);
  1. Remplacer les deux appels directs à car_thread dans le main par des appels à pthread_create pour lancer car_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 ? 🙀

  1. Changer le deuxième appel à pthread_create pour stocker le thread id de la deuxième voiture.

  2. 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 retourne car_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);
  1. 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.

  1. On va donc faire un do { ... } while(...); dans notre main.

  2. Dedans, on va récupérer un caractère de stdin avec input = fgetc(stdin);

  3. Si la touche est j, on lance une voiture sur la route du bas.

  4. Si la touche est k, on lance une voiture sur la route du haut.

  5. Si la touche est q, on sort de la boucle.

  6. À 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


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

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