YooGoo

 
 
Bienvenue sur YooGoo
 
 

 
 
News

Description du Projet
Avancement du Projet
L'équipe
Photos du Projet et du reste
 
 

 
 
Téléchargement

Le Client
Le Serveur
Les Sources Serveur
Les Sources Client
Cahiers des Charges
Soutenance 1
Soutenance 2
Soutenance 3
Soutenance Finale
 
 

 
 
Contacts

Franck Tetzlaff CV Tessari Marco CV Bouhelier Stéphane CV Pouiller Jérôme CV
 
 

 
 
Sous-sections

Le Multithread

Définitions

Multithread

Le multithread était à la base créé pour les systèmes informatiques basés sur plusieurs processeurs. L'idée était de donner un groupe d'information à traiter à chaque processeur. Le système d'exploitation a donc un rôle primordial dans le multithread car c'est lui qui définit la planification des processeurs.

Le principe de l'exécution en parallèle est quelque chose de relativement nouveau et fonctionne aussi très bien sur des configurations munies seulement d'un processeur. Pour cela le système d'exploitation regroupe un certain nombre de processus qui sont les programmes en cours d'exécution regroupant eux-mêmes des threads.

L'OS accorde un certain laps de temps pour l'exécution d'un thread en fonction de ses priorités. Avec ce principe on comprend mieux ce que signifie l'exécution en parallèle bien qu'il ne s'agisse pas vraiment de parallélisme dans un système monoprocesseur.

Thread

Un thread est une séquence d'instructions exécutées à l'intérieur du contexte d'un processus. Par exemple dans notre serveur, on trouve à l'intérieur du thread canal, exclusivement des instructions visant à gérer les canaux disponibles.

Programme Monothread

C'est un programme codé en séquentiel, c'est-à-dire que les instructions sont exécutées les unes après les autres. Il y a un thread pour un processus.

Programme Multithread

C'est un programme utilisant plusieurs threads au sein d'un même processus et qui partage un même espace mémoire.

MT-Safe

Pour un programme multithread on parle de MT-Safe lorsqu'il fonctionne correctement alors qu'il partage des données "sensibles". Pour une librairie c'est à peu près le même principe. Elle est MT-Safe lorsque chaque appel de fonction n'engendre aucune perte de donnée dans le programme.

User-level Thread

Comme leur nom l'indique les threads User se situent au niveau de l'utilisateur c'est-à-dire au niveau du programme. Les User-level Threads ne sont visibles qu'à l'intérieur du processus dans lequel ils partagent toutes les ressources du processus (espace d'adressage ...) Chacun de ces threads possède les informations suivantes :

*
Un unique identificateur (Thread Id)
*
Etat du registre.
*
Ressource nécessaire pour supporter un flux de contrôle telle que la pile.
*
Une priorité et une planification.
*
Un emplacement spécial pour stocker des données particulières telles que les numéros d'erreur.

Ils sont rapides à synchroniser lorsque la synchronisation s'effectue au niveau utilisateur et non au niveau du kernel sans dépendance avec le système d'exploitation. Facilement gérable par la librairie des threads. Ce sont les threads gérés par les programmeurs d'applications.

Kernel-level Thread ou Lightweight processes (LWPs)

Les Kernel-level Thread comme leur nom l'indique se situent au niveau du kernel. Leur rôle est de faire le lien entre les User-level thread et le matériel (CPU). On les trouve beaucoup plus souvent sous le nom de Lightweight Process(LWPs). Il n'y a pas de mappage direct entre le thread user et le LWP. Le thread user doit être capable de changer librement de LWP dans la zone des LWPs. C'est le système d'exploitation qui décide quel LWP doit utiliser quel processeur et quand. Ils sont donc gérés par l'OS et non par le programme.

Bound et Unbound Thread

Ce sont des threads qui sont gérés, planifiés dans l'espace des PWLs. Les Unbound Threads sont plus légers que les PWLs. De plus, seulement si cela est nécessaire, un thread utilisateur peut être attaché définitivement à un PWL, c'est un bound thread.

Il existe une énorme différence dans la création et la synchronisation entre les bound threads et les unbound threads.


Tableau: Ce tableau illustre les différences à la création
Création Micro Secondes Rapport
Unbound Thread 101.0 -
Bound Thread 422.0 4.2
Fork() 3057.0 30.3



Tableau: Ce tableau montre lui les différences à la synchronisation
Création Micro Secondes Rapport
Unbound Thread 1.8 -
Bound Thread 48. 26.7
Fork() 105. 58.3


Il est donc important d'utiliser les Unbound thread le plus souvent possible. Les Unbound threads sont les threads créés par défaut.

Concepts de la programmation multithread

Tout d'abord pour créer un programme multithread, le programmeur doit raisonner en pensant au model multithread sans en abuser. C'est-à-dire qu'il est inutile de mettre des threads partout dans le programme, mais il faut savoir en placer là où il y a nécessité. Un programme doit utiliser les threads pour faire apparaître les activités parallèles à travers :

1)
La décomposition géométrique, c'est-à-dire répartir les données que le programme utilise, et faire en sorte que chaque processeur s'occupe d'une zone de calcul prédéfinie. La décomposition géométrique découpe le projet en grosses parties.

2)
La décomposition des fonctions, c'est-à-dire répartir les fonctions de telle sorte que chaque processeur s'occupe si possible d'une zone de calcul. Dans cette décomposition le projet est encore redivisé.

C'est après avoir effectué ces décompositions qu'on peut savoir quelle partie du projet aura besoin de threads.
La librairie des threads est indispensable pour :

*
Créer et assigner des threads aux activités parallèles de l'application
*
Organiser la coopération et la synchronisation des threads lorsqu'elles accèdent à des données partagées ou des ressources du processus.
*
Faire en sorte que le programme multithread utilise le mieux possible une machine multiprocesseur.

Création et gestion des threads

Pour pouvoir utiliser les threads dans notre code, il y a un certain nombre de fonctions à savoir utiliser. Il y a la création de thread, la destruction, la mise en pause, la reprise et la synchronisation... Pour ce qui est de la synchronisation, il faut savoir utiliser d'autres objets qu'on nomme objets de synchronisation. Il peut s'agir des mutex ou des sémaphores. Il faut alors savoir les créer et les implémenter là où il faut dans le programme.

Création de threads

Pour créer un thread sous Windows, il faut appeler la fonction CreateThread qui se trouve dans l'API Windows. Tout ce qui va être dit ne sera vrai que sous le système d'exploitation de Microsoft. En ce qui concerne les autres systèmes d'exploitation, un paragraphe sera consacré au portage du serveur sous Unix. La fonction CreateThread() prend 6 paramètres.

*
Le premier est la taille de la pile qui sera attribuée au thread.
*
Le second est l'attribut de sécurité, qui sert à définir par exemple qui à le droit de détruire le thread.
*
Le troisième est l'adresse de démarrage du thread, on passe à ce paramètre l'adresse d'une fonction du programme.
*
Le quatrième représente les valeurs communiquées au point de démarrage du thread, c'est-à-dire les paramètres de la fonction principale du thread.
*
Le cinquième indique la priorité du thread. Ce paramètre est utile pour le système d'exploitation.
*
Le dernier paramètre sert à stocker un identificateur de thread.

Cette fonction retourne 0 en cas d'échec ou le handle du thread en cas de réussite.

Dans le serveur YooGoo la création de thread intervient au chargement du serveur pour la création des threads canal et user. La partie communication, Winsock, se trouve également dans un thread. Par ailleurs la thread Winsock a la possibilité de créer des threads lorsque le nombre de connectés dépasse un multiple de 64. Notre serveur compte donc au minimum 2 threads.

Destruction d'un thread

Pour sortir du thread en cours, il est possible d'utiliser la fonction ExitThread(), qui ne prend aucun paramètre et qui est l'équivalent d'un break.

Mais parfois il est utile de pouvoir terminer un thread sur ordre d'un autre. Par exemple pour l'extinction du serveur, le thread principal donnera lui-même l'ordre de détruire les threads encore actifs. Pour faire cela il utilise la fonction TerminateThread().

Cette fonction est assez simple et ne prend que 2 paramètres :

*
L'handle du thread à terminer
*
Le code de sorti du thread concerné.

Les valeurs de retour sont TRUE ou FALSE. Le code de retour est donc une valeur booléenne définit dans la librairie winbase.h.
Dans le serveur la destruction ne s'effectue donc que lorsque le serveur s'éteint ou lorsque le nombre d'utilisateur passe en dessous d'un multiple de 64.

Définir la priorité

La priorité d'un thread est quelque chose de vraiment important car c'est une information qui interagit directement avec le système d'exploitation. Il est possible de voir les différentes priorités attribuées à des programmes à travers le gestionnaire des taches de Windows par exemple. Dans notre projet, il n'a pas encore été pensé de définir une priorité aux threads, mais cela pourrait peut-être changé. Le programme qui est présenté à la soutenance utilise lui le système de priorité.

Pour définir une priorité, le plus simple est d'attribuer une priorité à la création du thread car il n'est pas recommandé de changer la priorité d'un thread durant son exécution. Cependant comme le montre le petit programme test, cela est tout à fait possible. Pour cela, il faut utiliser la fonction SetThreadPriority() qui prend les paramètres suivants :

*
Le handle du thread
*
La valeur de la priorité

Il existe des constantes textes déjà préfaites dans la librairie winbase.h mais il faut savoir que le niveau de priorité sous Windows s'étale jusqu'à la valeur 31 qui correspond à la valeur RealTime fortement déconseillée car il devient alors quasiment impossible d'effectuer quelque chose d'autre avec l'OS pendant ce temps là. La priorité peut également être négative pour désigner une utilité moindre.

Statistiques

Les threads pourront nous aider en ce qui concerne les statistiques. En effet les threads regroupent un grand nombre d'informations assez importantes aussi bien pour les personnes qui utiliseront YooGoo que pour nous programmeurs. Tout d'abord à l'intérieur du processus, il est possible de savoir pour chaque thread :

*
Sa date de création
*
Le temps d'occupation du processeur
*
Et le mode dans lequel il a utiliser le processeur (Utilisateur ou Superviseur)

Toutes ces informations utilisées intelligemment permettront par exemple de savoir depuis combien de temps le serveur YooGoo tourne, quel thread est le plus souvent demandé...

Pour obtenir toutes ces informations, il suffit d'appeler la fonction GetThreadTimes() avec les paramètres suivants :

*
L'handle du thread
*
Variable qui stockera la date de création du thread
*
Variable qui stockera la date de destruction du thread
*
Variable qui stockera le temps passé en mode superviseur
*
Variable qui stockera le temps passé en mode utilisateur

La valeur de retour est un booléen qui est à TRUE si l'opération s'est bien passée.

Suspension et reprise d'un thread

L'un des objectifs de chaque programme et particulièrement du notre est de faire en sorte que lorsque le serveur n'ait rien à faire, qu'il n'utilise pas 100% du processeur, à travers une boucle infinie par exemple. Pour cela il faut utiliser la programmation événementielle. C'est ce que nous avons fait pour le serveur.

Il faut également savoir qu'il est possible sous une fenêtre DOS de pouvoir recréer de l'événementiel en analysant les entrées qui sont présentes dans la fenêtre DOS (entrée clavier / souris par exemple). La fonction select dans la thread Winsock fait également la même chose. Elle bloque un certain temps (en fonction du réglage du timer) et n'utilise que très peu de ressources systèmes.

Enfin la méthode la plus propre est de suspendre un thread lorsqu'il n'a rien à faire. C'est le cas de la thread de la file des messages. Lorsqu'aucun message n'est présent dans les files de messages des utilisateurs il est inutile de faire tourner la thread en boucle, il faut donc pouvoir la mettre en pause. Mais il faut obligatoirement savoir quand la réveiller. Il faut la reprendre lorsqu'un message est enfilé. A chaque défilage on décrémente un compteur de message. Une fois arrivé à 0, on suspend le thread qui n'occupe plus de ressources au niveau du processeur. Et le thread est réveillé après enfilage.

La fonction pour suspendre un thread est SuspendThread(HANDLE hThread) qui retourne -1 en cas d'erreur.

La fonction pour reprendre l'exécution d'un thread est ResumeThread(HANDLE hThread) qui retourne -1 en cas d'erreur.

Synchronisation

Les threads permettent donc de faire des choses qui n'étaient pas possible avant ou difficilement, car l'exécution en séquentielle pouvait bloquer comme c'était le cas dans le client par exemple où la fonction Select figeait l'affichage lorsqu'elle se trouvait dans le thread principal. Cependant il est indispensable de synchroniser les threads entre eux car s'il arrive qu'ils appellent simultanément la même ressource les conséquences peuvent être catastrophiques. Par exemple si une personne modifie un élément de la base de données alors qu'une autre personne essaie de lire la même entrée, le résultat est imprévisible. Il se peut que l'élément renvoyé soit simplement erroné mais il est fort probable que le serveur plante.

Pour cela il est nécessaire d'utiliser des objets de synchronisation. Il en existe plusieurs, mais seulement deux nous intéressent, il s'agit du Mutex et du Sémaphore.

Mutex

Par définition un Mutex autorise l'accès mutuellement exclusif, c'est-à-dire qu'il n'est possible qu'à une personne d'accéder à la ressource partagée. Un mutex ne peut appartenir qu'à un seul thread à la fois. Cependant, si un thread possède un mutex et qu'un autre thread en demande la propriété, le thread demandant ne peut l'acquérir que quand le thread propriétaire libère sa propriété.

Pour pouvoir utiliser les mutex, la première opération à effectuer est de le créer. Pour cela il faut faire appel à la fonction CreateMutex() qui prend trois paramètres :

*
Les attributs de sécurité qui déterminent si le Mutex est " héritable " c'est-à-dire s'il peut être appelé par un thread d'un autre processus par exemple
*
Le thread possesseur initial du thread
*
Le nom de l'objet de synchronisation (ex : " bddMutex ")

La valeur de retour en cas de succès est l'handle du mutex, sinon le code retour est NULL.

Il faut également pouvoir libérer le mutex après la libération de la propriété. Pour cela il faut faire appel à la fonction ReleaseMutex() qui ne prend en paramètre que l'handle du Mutex à libérer. Le Mutex est alors passé au thread en attente s'il y en a.

Sémaphore

Par définition un Sémaphore autorise l'accès à un nombre défini de threads. Un objet Sémaphore démarre avec un compte initial attribué à sa création. Chaque fois qu'un thread devient propriétaire de l'objet (en utilisant la fonction d'attente), le compteur du sémaphore est décrémenté. Chaque fois qu'un thread libère la propriété, le compteur du sémaphore est incrémenté. Si celui-ci est à 0, le sémaphore est bloqué sur l'état non signalé et aucun autre thread ne peut y accéder.

De même que pour les Mutex il faut pouvoir créer l'objet sémaphore. Pour cela il faut utiliser la fonction CreateSemaphore() qui avec le même principe que les mutex prend quatre paramètres :

*
Les attributs de sécurités
*
La valeur initial du compteur
*
La valeur maximum du compteur
*
L'identifiant de l'objet comme pour le mutex

La valeur de retour est l'handle du sémaphore ou NULL en cas d'échec.

Toutefois il est intéressant de remarquer qu'un mutex n'est rien de plus qu'un sémaphore avec un compteur initial à 1 et un maximum de 1. Cette solution va être utilisée pour la compatibilité et le portage sous Unix. De plus c'est la méthode employée dans le petit exemple sur les threads.

La fonction de ReleaseSemaphore() doit être appelée pour désigner la libération de la ressource. Elle prend 3 paramètres :

*
L'handle du sémaphore
*
Un nombre désignant la valeur de l'incrémentation
*
Un paramètre de sortie qui récupère la valeur du compteur avant l'incrémentation

La valeur de retour est 0 en cas d'échec et n'importe quelle autre valeur en cas de réussite.

Implémentation des objets de synchronisation dans le programme

Après avoir créé les objets, il faut pouvoir les appeler juste avant la propriété à protéger. Il faut donc bien veiller à n'oublier aucune zone sensible du programme.

Les tests effectués juste avant la propriété sont en fait des appels aux objets de synchronisation. Ces appels sont effectués par la fonction WaitForSingleObject() qui prend 2 paramètres :

*
L'handle de l'objet Mutex ou Sémaphore.
*
Un time-out qui définit l'attente maximale d'un thread avant de zapper la propriété. Cette valeur est un double mot qui représente une valeur en millisecondes Dans notre cas ce paramètre est égal à 0, ce qui signifie que le time-out est infini.

Il existe 4 valeurs de retour :

*
WAIT_ABANDONED : c'est un message d'erreur dû à un problème de droit
*
WAIT_OBJECT_0 : c'est la valeur en cas de succès
*
WAIT_TIMEOUT : c'est un message d'erreur qui signifie que le délai d'attente est passé et que le programme continue son exécution
*
WAIT_FAILED : c'est un message d'erreur dû au fait que l'handle passé n'est pas valide.

La fonction WaitForSingleObject() n'attend que pour un objet donné, ce qui est suffisant dans notre cas. Pour pouvoir attendre plusieurs objets de synchronisation il faut appeler la fonction WaitForMultipleObject().

 
 

 
 
Documentation

FAQ
Conception du Protocole
SDK en Ligne
Télécharger SDK
Cahiers des Charges
Soutenance 1
Soutenance 2
Soutenance 3
Soutenance Finale

Comment marche...
Les Canaux
Le Profile
L'administration
La Base de Données
La Cryptographie
Winsock
Le Multithreading
 
 

 
 
Liens

Epita
EpiTarget
Hallucinetik - Zone 42
poupouill.fr.st Minosis (Spé C2)
La Spé C1
La Sup C1
 
 


 
 
made in Epita Powered by ApachePHP Scripting Language

All logos and trademarks in this site are property of their respective owner. The comments are property of their posters, all the rest © 2000 by YooGoo Team
Des remarques, des question, des choses pas claires? Hargos@ifrance.com