Jeux Libres

Plateforme de création de jeux vidéo (Le site est en cours de création / réorganisation)


» Les Tutoriaux » Programmez en multitâches !

Programmez en multitâches !


Dans ce chapitre, nous allons apprendre à nous servir des taches.

Nous découvrirons des phénomènes assez surprenant que nous tenterons d'expliquer au travers d'exemples simple.
Et nous verrons pourquoi les taches nous sont indispenssable pour la création de jeux vidéo plus complexe.

Les taches nous serrons bientôt indispenssable pour nos jeux vidéo en réseau ! Vous comprendrez pourquoi très bientôt !




Qu'est ce qu'une tache ?


Une tache sert à effectuer quelquechose le temps de faire autre chose.
Autrement dit, votre programme prendra deux chemins différents au cours de son execution.




Cela nous serra bientôt très utile lorsque nous créerons des jeux vidéo en réseau.
Le programme en lui même affichera les personnages à l'écran tendis qu'une tache se chargera de récupérer la position actuelle des autres adversaires.

Pour commencer simplement avec les taches, nous allons devoir retourner à la console.
Mais dite vous que c'est pour la bonne cause : ce chapitre est en quelque sorte une porte à ouvrir pour entrer dans le monde de la programmation de jeux vidéo en réseau.


Installation de POSIX Threads


Comme je vous l'ai dis, votre system d'exploitation est multitaches.
Il contient tout ce qu'il faut pour découper le temps et le partager entre les différentes taches qu'il doit gérer.

Pour utiliser cette possibilité à l'intérieur de notre programme, nous allons avoir besoin d'une librairie. Il s'agit de la librairie POSIX Threads : une librairie libre, sous licence LGPL, et multiplateforme.

Pour la suite, je vais considérer que vous êtes sous Windows.
Si vous êtes sous Linux, adaptez vous en conséquence.

Télécharger POSIX Threads



Je vais donc vous demander de télécharger la dernière version de POSIX Threads.
La page de téléchargement de POSIX Threads, la voici :

Télécharger POSIX Threads for WIN32

Dans la partie 'Download', cliquez sur le premier lien et choisissez la dernière version de POSIX Threads.

Pour pouvoir continuer, je doit m'assurer que vous aillez bien téléchargé le bon fichier.
Si vous avez un doute, ne vous prenez pas la tête, téléchargez POSIX Threads en cliquant ici.

Attention : Si vous êtes sous linux, le fichier à télécharger et la marche à suivre sont différents. Si vous avez un doute sur votre system d'exploitation, faite le teste en cliquant ici.

Installer POSIX Threads



Maintenant que vous êtes en possession de POSIX Threads, nous allons voir comment l'installer.
Enfait, la procédure d'installation de POSIX Threads ressemble beaucoup à celle de la SDL.

La seule grosse différence, c'est qu'ici il s'agit d'une archive SFX. Ce qui veux dire que nous n'aurrons pas besoin de logiciel de décompression, c'est l'archive elle même qui contient le module de décompression.


Création du dossier temporaire



On va donc créer un dossier pour y décompresser l'archive à l'intérieur.

Je le cré où le dossier ? Je l'appel comment ?

C'est vous qui choisissez. En ce qui me concerne, j'ai créé un nouveau dossier que j'ai appelé librairie_pthreads à l'interrieur de Mes documents. Ensuite, placez y l'archive.



Extraction du contenu de l'archive



Ensuite, on extrait le contenu de l'archive ! Pour cela, rien de plus simple : il suffi d'executer l'archive SFX et de cliquer sur Extract.

Par defaut, l'archive extrait son contenu dans le dossier où elle est placé. Cependant, vous pouvez choisir l'emplacement en cliquant sur Browse.



Au final, le contenu de l'archive se retrouve dans le dossier.


Résultat des courses !



De cette oppération, il en résulte 3 dossiers.
Nous n'allons avoir besoin que d'un seul de ces dossiers : Pre-built.


Contenu du seul dossier qui nous intéresse : Pre-built

Installation de POSIX Threads dans l'IDE



Contrairement à la SDL, nous ne créerons pas de nouveau dossier pour installer POSIX Threads.
Copiez le contenu du dossier Pre-built\include\ (les .h) directement dans le dossier mingw32\include\.


Pour finir, copiez les .lib et .a du dossier Pre-built\lib\ dans le dossier mingw32\lib\ de votre IDE.

Vous voici maintenant près à utiliser POSIX Threads dans vos projets. Par compte, n'oubliez pas de placer les .dll du dossier Pre-built\lib\ dans le dossier de votre programme. Aussi, nous verrons un peu plus loin qu'il faut linker (prononcez linké) la librairie pour compiler le programme qui l'utilise.

Le dossier temporaire que nous avons créé au début, nous ne nous en servirons plus. Vous pouvez donc le supprimer après avoir pris soin de mettre les .dll de côté pour vos futurs projets.


Créer une tache


Nous venons d'installer POSIX Threads. Maintenant, on va l'utiliser !

Créer une tache



Créons un nouveau projet et reprenons notre code minimal que nous avons vu au tout début.
Le voici :

Code : C
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
 
  system("PAUSE");     
  return 0;
}


Pour créer une tache, nous allons avoir besoin d'inclure la librairie pthread : #include <pthread.h> :

Code : C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> // Inclusion de la librairie pthread

int main(int argc, char *argv[])
{
 
  system("PAUSE");     
  return 0;
}


Pour fonctionner, une tache a besoin d'être définie par une fonction. C'est cette fonction qui serra executé en parallèle du reste du programme. Pour s'executer, cette fonction a besoin d'espace : une variable.

Cette variable doit être de type pthread_t.
Et la fonction doit avoir la signature suivante :

Code : C
static void fonction_tache (void *pointeurDonnees)


Voici un exemple :

Code : C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> // Inclusion de la librairie pthread

static void f_tache (void);

int main(int argc, char *argv[])
{
  pthread_t v_tache; // On declare une variable pour la tache

  system("PAUSE"); // Et on fait une pause
  return 0; // Puis on termine le programme
}

// Cette fonction fera office de tache
static void f_tache (void)
{
    // Cette tache affiche du texte
    printf ("Texte de la tache.\n");
}


Astuce : Pour ma tache, j'ai créé une variable v_tache et une fonction f_tache. S'il s'agissait d'une tache de reception de données, j'aurrai créé v_receptionDonnees et f_receptionDonnees.

Si vous compilez et executez ce code, vous ne verrez rien. Le programme declare une variable v_tache et se termine. Aucune tache n'a été executé.

Pour executer une tache, il va faloir faire appel à une fonction.
Cette fonction aurra pour effet de déclancher la tache.

Déclancher la tache



Nous venons de voir comment créer une tache.
Vous n'avez peut-être pas encore très bien compris. Pas de panique, vous comprendrez mieux dans quelque temps.

Maintenant que cette tache est créée (1), on va la déclancher (2).
La fonction qui permet de déclancher une tache, c'est pthread_create.

Pour déclancher la tache f_tache, il faut faire :

Code : C
pthread_create (&v_tache, NULL, f_tache, NULL); // On déclanche la tache


Comme ceci :

Code : C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> // Inclusion de la librairie pthread

static void f_tache (void);

int main(int argc, char *argv[])
{
  pthread_t v_tache; // On declare une variable pour la tache

  pthread_create (&v_tache, NULL, f_tache, NULL); // On déclanche la tache

  system("PAUSE"); // Et on fait une pause
  return 0; // Puis on termine le programme
}

// Cette fonction fera office de tache
static void f_tache (void)
{
    // Cette tache affiche du texte
    printf ("Texte de la tache.\n");
}


Lorsqu'on écrit f_tache, cela envoi l'adresse de cette fonction. C'est comme ça que pthread_create sait où se trouve la fonction à executer. (la tache)

Avant de pouvoir compiler ce programme, vous devrez linker certain fichiers.

Si vous êtes sous Dev-C++ : Allez dans le menu Projet -> Options du projet (ou Alt+P) puis cliquez sur l'onglet paramètres (en haut).


Options du projet (paramètres) : ce menu sert à linker les librairies qui serviront à la compilation.

Pour ajouter la librairie, cliquez sur Ajouter fichier et ajoutez les fichiers libpthreadGC1.a, libpthreadGCE1.a, pthreadVC1.lib et pthreadVSE1.lib du dossier mingw32\lib\ (ceux qu'on avait copier dans la première partie).

Si vous êtes sous Code::Blocks : Dans le menu Build -> Compiler Options, cliquez sur l'onglet Linker puis ajoutez les fichiers précédament cités en cliquant sur Add.


Compiler Settings (Linker) : ce menu sert à linker les librairies qui serviront à la compilation.

Vous voici maintenant près à compiler. allez y !


Execution du programme : 2 cas possibles !


A ce stade, 2 cas sont possible. Soit le programme affiche le "Texte de la tache.\n" et fait une pause ; soit le programme se met en pause d'abord et affiche le "Texte de la tache.\n" pendant la pause.

Mais ce n'est pas possible, le programme ne peut pas être en pause et afficher du texte pendant cette pause. Si ?

Et bien si !
Reprenons notre programme :

Code : C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> // Inclusion de la librairie pthread

static void f_tache (void);

int main(int argc, char *argv[])
{
  pthread_t v_tache; // On declare une variable pour la tache

  pthread_create (&v_tache, NULL, f_tache, NULL); // On déclanche la tache

  system("PAUSE"); // Et fait une pause
  return 0; // Puis on termine le programme
}

// Cette fonction fera office de tache
static void f_tache (void)
{
    // Cette tache affiche du texte
    printf ("Texte de la tache.\n");
}


J'aimerais attirer votre attention sur cette partie du code :

Code : C
  pthread_create (&v_tache, NULL, f_tache, NULL); // On déclanche la tache
  system("PAUSE"); // Et fait une pause


Ce code déclanche la tache qui écrit "Texte de la tache.\n" puis met le programme en pause ensuite.

J'attire votre attention sur le sens de ma phrase. Je n'ai pas dis que le programme afficherait "Texte de la tache.\n" puis se metterait en pause ; j'ai bien dis qu'il déclanchait la tache qui écrirait "Texte de la tache.\n" puis se metterait en pause. L'écriture du "Texte de la tache.\n" peut donc avoir lieu bien plus tard, même après la pause !

Voici donc les 2 résultats possibles que peut donner l'execution de ce programme :

Premier résultat :

Texte de la tache.
Appuyez sur une touche pour continuer... _


Second résultat :

Appuyez sur une touche pour continuer... Texte de la tache.
_


Chez moi, sous Windows XP, la pause est tellement longue à s'afficher que je me retrouve toujours dans le premier cas. Il est donc très probable que ce soit la même chose pour vous.

Si vous voulez retarder l'écriture du "Texte de la tache.\n", vous pouvez endormir la tache pendant 1000 millisecondes (1 seconde) avant d'écrire le texte. Cela permet au programme de se mettre en pause avant que la tache n'écrive le texte.

Pour celà, utilisez la fonction sleep().

Comme ceci :

Code : C
// Cette fonction fera office de tache
static void f_tache (void)
{
    // On attend une seconde
    sleep (1000);

    // Et on affiche le texte de la tache
    printf ("Texte de la tache.\n");
}


Cette fois-ci, le programme affichera la pause avant le "Texte de la tache.\n" :

Appuyez sur une touche pour continuer... Texte de la tache.
_


Et si on schématise !



Faut avouer qu'au début, c'est pas facil de s'imaginer que le programme s'execute avec une tache parallèle.

Je vais donc vous schématiser tout ça pour que ce soit clair. Ce serait génant de rester bloqué là, si proche de la programmation de jeux en réseau.

Voyons ce que donne ce code graphiquement :

Code : C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> // Inclusion de la librairie pthread

static void f_tache (void);

int main(int argc, char *argv[])
{
  pthread_t v_tache; // On declare une variable pour la tache

  pthread_create (&v_tache, NULL, f_tache, NULL); // On déclanche la tache

  system("PAUSE"); // Et fait une pause
  return 0; // Puis on termine le programme
}

// Cette fonction fera office de tache
static void f_tache (void)
{
    // Cette tache affiche du texte
    printf ("Texte de la tache.\n");
}


Le programme déclanche la tache. Cette tache affiche le "Texte de la tache.\n" le temps que le programme met en place la pause.
Apparament, la pause met du temps à ce mettre en place : le "Texte de la tache.\n" s'affiche avant la pause.

On obtient ceci :


Ce qui donne :

Texte de la tache.
Appuyez sur une touche pour continuer... _


Mais il est tout à fait possible que chez vous ce soit le contraire. Peut-être que chez vous la pause ne prend que très peu de temps à se mettre en place. Dans ce cas vous vous retrouvez dans ce cas de figure :


Dans ce cas, vous avez dû obtenir le résultat suivant :

Appuyez sur une touche pour continuer... Texte de la tache.
_


Affin de mettre en évidence la tache parallèle, je vous ai proposé de retarder la tache avec la fonction sleep() avant d'écrire le "Texte de la tache.\n".

Cette fois-ci, il n'y a quasiment aucun doute, la pause sera faite bien avant l'écriture du "Texte de la tache.\n".

Schématisé, ça donne ça :


Et le résultat resemble beaucoup à celui du cas précédent :

Appuyez sur une touche pour continuer... Texte de la tache.
_


La petite nuance avec le cas précédent c'est que le retard du "Texte de la tache.\n" est un peu trop flagrand.

Le retard de la tache nous a permis de la mettre en évidence.
Vous verrez plus tard que l'ordre d'execution des instructions entre les différentes taches et celle du programme n'aurras pas telment d'importance. Dans le cas contraire nous apprendrons à synchroniser certaines taches.

Ce que vous savez faire !



Vous savez maintenant créer et déclancher des taches (des fonctions) en parallèle du reste du programme. Ces taches peuvent effectuer des calcules, afficher du texte, manipuler des variables... A priorie, on peut tout faire avec !

Ce qui nous reste à voir, c'est comment faire des taches capables d'agir sur les données du programme.


Travailler en tache : le problème


Dans cette dernière partie, nous allons apprendre à manipuler une donnée du programme depuis une tâche.

Quel intérêt ?

L'intérêt, c'est que le programme pourra poursuivre son execution en utilisant la donnée sans avoir à ce soucier du reste. Et si la donnée est modifié par une tache, elle modifiera l'execution du programme en conséquence.


Exemple schematisé d'un programme qui dépend
d'une valeur pouvant être modifiée à tout moment par une tâche.

Mais où veux tu en venir ?

Et bien ... Au réseau !

Très bientôt, vous verrez que cette technique vous sera indispenssable !

Le jeu poursuivera son execution on affichant les adversaires à la position indiqué par une donnée. Pendant ce temps, une tache se chargera de maintenir à jour cette donnée en restant informé des moindres déplacements des autres joueurs.


Image expliquative du cheminement de l'information dans un jeu en réseau.

Le problème !



Je vais tout de suite vous poser le problème, vous comprendrez l'utilité de travailler en tache.

Imaginons que l'on veuille faire un programme qui affiche à chaque seconde la valeur d'un variable.
Très bien, il suffit de faire ceci :

Code : C
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    long variable = 219;

    while (1)
    {
        sleep (1000); // Chaque seconde
        printf ("%d\n", variable); // On affiche la valeur de la variable
    }

    system("PAUSE");     
    return 0;
}


Et voici le résultat après quelques secondes d'éxecution :

219
219
219
219
219
_


On va compliquer un peu les choses : une tache va incrémenter cette valeur toutes les 300 ms (0,3 seconde).
Je vous laisse chercher un peu...

C'est bizard, ça ne s'incrémente pas !

Je me met à votre place : vous avez dû faire quelque chose proche de ça :

Code : C
/*
    Ce code a été écrit dans un but pédagogique.
    Ce code ne fonctionne pas.
*/


#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> // Inclusion de la librairie pthread

static void *f_incrementer (void);

int main(int argc, char *argv[])
{
    long variable = 219;

    // On déclanche la tâche qui incrémente la valeur de la variable
    pthread_t v_incrementer;
    pthread_create (&v_incrementer, NULL, f_incrementer, NULL);

    while (1)
    {
        sleep (1000); // Chaques secondes
        printf ("%d\n", variable); // On affiche la valeur de la variable
    }

    system("PAUSE");
    return 0;
}

// Tache qui incrémente la valeur de la variable
static void *f_incrementer (void)
{
    // Une erreur de compilation nous
    // oblige à créer cette variable
    long variable;

    while (1)
    {
        sleep (300); // Toutes les 300 ms
        variable ++; // On incrémente la valeur de la variable
    }
}


Le soucis, c'est qu'on déclare une variable variable dans le programme principal ; et une autre variable variable dans la tâche.

Il y a donc 2 variable différentes qui désigne 2 endroits différents dans la mémoire.


L'astuce


Heureusement, il existe une astuce.
Nous allons utiliser le dernier paramètre de la fonction pthread_create (celle qui sert à déclancher la tache).

Jusque là, je vous avais dis de mettre NULL et de ne pas trop vous poser de question.
Nous allons maintenant voir l'utilité de ce paramètre.

A quoi sert ce 4ème paramètre ?

Ce 4ème paramètre va nous servir à transmettre une adresse mémoire à la tâche. Vous l'aurez compris, nous nous en servirons pour transmettre l'adresse mémoire de la variable à incrémenter.

On y va :

Code : C
// On déclanche la tache en lui envoyant l'adresse de la variable
pthread_create (&v_incrementer, NULL, f_incrementer, &variable);


Dans la tâche, on récupère cette adresse avec son paramètre sous forme de pointeur de type void.

On continue :

Code : C
static void *f_incrementer (void* p_variable)


Attention au Warning : Il est tentant de récupérer cette adresse avec un pointeur de type long. Pourtant, si vous ne voulez pas tomber sur un Warning, je vous conseil de vous débrouiller autrement.

Voici une solution simple d'incrémenté la valeur situé à l'adresse reçu :

Code : C
// Tache qui incrémente la valeur de la variable
static void *f_incrementer (void* p_variable)
{
    // On construit un pointeur sur la variable avec l'adresse que l'on a récupéré
    long *variable = p_variable;

    while (1)
    {
        sleep (300); // Toutes les 300 ms
        (*variable) ++; // On incrémente la valeur de la variable pointé par le pointeur
    }
}


Le code final !



Code : C
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> // Inclusion de la librairie pthread

static void *f_incrementer (void* p_variable);

int main(int argc, char *argv[])
{
    long variable = 219;

    // On déclanche la tâche qui incrémente la valeur de la variable
    pthread_t v_incrementer;
    pthread_create (&v_incrementer, NULL, f_incrementer, &variable);

    while (1)
    {
        sleep (1000); // Chaques secondes
        printf ("%d\n", variable); // On affiche la valeur de la variable
    }

    system("PAUSE");
    return 0;
}

// Tache qui incrémente la valeur de la variable
static void *f_incrementer (void* p_variable)
{
    // On construit un pointeur sur la variable avec l'adresse que l'on a récupéré
    long *variable = p_variable;

    while (1)
    {
        sleep (300); // Toutes les 300 ms
        (*variable) ++; // On incrémente la valeur de la variable
    }
}


Et après quelques secondes, voici le résultat :

222
225
228
232
235
238
241
245
_


La fonction sleep n'est pas une fonction parfaite, elle dépend du system d'exploitation et de l'éxécution des autres programmes. Il est donc probable que vous n'obteniez pas le même résultat. Et 2 executions de ce même programme de donne pas systématiquement le même résultat.

Expliquation et interprétation du résultat



Ce programme affiche la valeur de la variable chaques secondes.
Pendant ce temps, cette variable est incrémenté plusieurs fois par secondes : entre 3 et 4 fois.

Parfois, 4 incrémentations on lieu entre 2 affichages. Cela vient du fait qu'il y a un peu plus de 3 fois 300 ms dans une seconde.


La fonction sleep n'étant pas parfaite, l'éxécution du programme et de la tache ne peuvent pas être parfaitement concurante (loin de là). L'une des deux éxécutions a donc forcément de l'avance sur l'autre.

D'ailleur, dans le résultat que j'ai relevé (un peu plus haut), on constate que l'éxécution de la tache est en retard sur celle du programme : La valeur 241 aurrait dû passer à 242 avant que le programme ne l'affiche.




Ce chapitre a été long et très riche en connaissance. Vous allez maintenant pouvoir passer au réseau.

Plus tard, nous verrons comment combiner les tâches et le réseau. Et ceci aboutira sur un TP très simple qui mettera en oeuvre ces deux notions.



Rédigé par David



Hébergeur du site : 1and1.fr



Site de création de Jeux Vidéo
Apprenez à créer vos propres Jeux Video

A propos de la construction du site...
352245 pages ont été consultées sur le site !
Dont 242 pages pendant les 24 dernières heures.

Page générée en 0.127 secondes


Nos partenaires
- Otium Production : Aide aux débutants à créer leurs jeux
- Construis ton jeu en PHP : Apprenez à créer votre jeu en PHP
- A.C.S.E.L. : Club de patinage artistique de Caen


  © 2005-2008 www.jeux-libres.com - Toute reproduction totale ou partielle du contenu de ce site est strictement interdite.