MultiThread
Les fonctions de gestion des threads:
Création d'un thread:
CreateThread(): Permet de se créer un thread. Nous devons pour cela définir un point d'entrée pour ce thread. Ce point d'entrée n'étant ni plus ni moins qu'un code recevant des paramètres.
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
Les paramètres:
lpThreadAttributes : Pointeur sur une structure du type SECURITY_ATTRIBUTES. Nous le positionnerons à NULL.
dwStackSize : Taille initial en octet de la pile. (attention, le système
pourra arrondir cette taille). Nous le positionnerons aussi à NULL pour
récupérer la taille de pile correspondant à notre application.
lpStartAddress : Paramètre important, c'est le point d'entrée,
le code initital de notre thread.
lpParameter : Paramètre à passer au code initial de notre thread.
dwCreationFlags Flags pour déterminer l'état initial du thread. Si NULL, le thread démarre immédiatement. Si CREATE_SUSPENDED, le thread est créé mais il est dans un état "suspendu", Il ne tournera donc pas jusqu'à ce que nous nous le lancions via la fonction ResumeThread().
lpThreadId : Variable en output qui recevra l'identificateur du thread (attention,
ce n'est pas le handle). Si la valeur est NULL, éh bien vous ne récupérerez
pas cette valeur...
La fonction retourne le handle du thread si pas de problème, sinon la valeur est NULL...
Petite précision,
les ressources (fichiers, socket, ...) ouvertes sont accéssibles dans
le thread créé.
Jamais essayé : pour créer un thread il existe aussi la fonction void _beginthread(ThreadProc, uiStackSize, pParam);
La procédure initiale d'un thread:
Ce qui m'amène à parler de la procédure initiale associée à notre nouveau thread.
Son en-tête est très simple:
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
// Notre code ici
// C'est terminé, le thread va être arrêté par le système dès que cette fonction sera terminée. Le return doit retourner 0 ou un code erreur...
return 0;
}
Remarque: Toutes variables déclarées dans cette procédure initiale ne seront connues que du thread les ayant déclarées.
Mise en sommeil:
Un thread ne peut s'exécuter que pendant un certain temps maximum qui peut varier. Mais si le traitement de ce thread se termine avant ce temps maximum, il serait dommage de ne pas rendre la main à un autre thread et éviter ainsi de perdre du temps CPU à ne rien faire.
Il existe plusieurs solutions:
Sleep() : Cette fonction va mettre en sommeil ce tread, pendant une certaine période qui sera indiquée en milliseconde. A la fin de cette mise en sommeil, il sera automatiquement remis dans la file d'attente pour être de nouveau exécuté.
VOID Sleep(
DWORD dwMilliseconds
);
Si la durée indiquée est 0, le thread ne consommera pas tout son temps et permettra à un autre thread de démarrer. C'est toujours cela de gagner.
On pourra aussi utiliser SleepEx avec les fonctions ReadFileEx ou WriteFileEx dans un même thread. Ainsi, le thread s'endormiera durant l'opération I/O. Il se réveillera lorsque la durée sera passée ou lorsque l'opération I/O sera terminée.
DWORD SleepEx(
DWORD dwMilliseconds,
BOOL bAlertable
);
La valeur retournée est 0 si la durée de mise en sommeil a expiré, ou WAIT_IO_COMPLETION lorsque l'opération d'I/O est terminée.
l'information WAIT_IO_COMPLETION ne pouvant se faire que si bAlertable est positionnée à vrai.
Les priorités :
Il y a deux niveaux utilisés pour la gestion des priorités d'un thread:
- La classe de priorité
- Le niveau de priorité dans cette classe
La classe de priorité étant positionnée sur le processus. Nous nous intéresserons ici au niveau de priorité dans la classe.
On peut lire ou positionner les niveaux de priorités:
Pour lire les priorités, on utilisera la fonction GetThreadPriority()
int GetThreadPriority(
HANDLE hThread // Handle du thread
);
Pour changer les priorités, on utilisera SetThreadPriority()
BOOL SetThreadPriority(
HANDLE hThread, // Handle du thread
int nPriority // Niveau de priorité
);
En retour, la fonction retourne THREAD_PRIORITY_ERROR_RETURN s'il y a des erreurs.
Voici les codes retournés: Il y a 7 niveaux de priorité sous Windows:
Code | Description |
THREAD_PRIORITY_IDLE | Priorité la plus basse |
THREAD_PRIORITY_LOWEST | La priorité est de deux points sous THREAD_PRIORITY_NORMAL |
THREAD_PRIORITY_BELOW_NORMAL | La priorité est un point sous THREAD_PRIORITY_NORMAL |
THREAD_PRIORITY_NORMAL | Priorité normale |
THREAD_PRIORITY_ABOVE_NORMAL | La priorité est un point au dessus de THREAD_PRIORITY_NORMAL |
THREAD_PRIORITY_HIGHEST | La priorité est de deux points au dessus de THREAD_PRIORITY_NORMAL |
THREAD_PRIORITY_TIME_CRITICAL | Priorité maximum |
Niveau de priorité de la plus basse à la plus haute dans les classes de priorités.
Terminaison d'un thread:
TerminateThread(): Cette fonction va détruire immédiatement un thread, mais il vaut mieux s'abtenir de l'utiliser si le thread fait des allocations mémoires par exemple. En effet, cette fonction détruit le thread, mais ne fait rien d'autre. Par conséquent, toutes allocations mémoires ou chargement de DLL ne seront pas libérés. Ce sera alors à vous de le faire...
Dès le lancement du programme, un nouveau thread est créé et va incrémenter un compteur.
Dans la fenêtre, un menu s'affiche dans lequel une option va permettre d'arrêter le thread. L'arrêt du thread provoquant l'affichage de ce compteur.
Une autre solution étant d'indiquer au thread qu'il doit se terminer. Or un thread s'arrête automatiquement lorsque le code de point d'entrée du thread se termine.
(Remarque:Il est tout a fait possible de suspendre un thread (vu un peu plus loin), dans ce cas, le thread est toujours présent, mais il n'exécute plus rien...il est en pause en gros !
Evidemment, dans l'exemple qui suit, je part sur le principe que le thread n'est jamais suspendu. Car si le thread est suspendu, il ne pourra jamais savoir qu'il doit se terminer !
Vous trouverez un exemple qui "résume" (l'action qui informe qu'il ne doit plus être en pause s'appelle résumer) d'abord le thread avant de l'informer qu'il doit se terminer dans le paragraphe "Suspendre ou résumer un thread").
Pour indiquer à un thread qu'il doit se terminer, nous pourrons utiliser plusieurs solutions dont:
- La création d'une variable globale qui indiquera la fin du Thread. Le thread doit alors périodiquement lire cette variable.
Voici un exemple simple. Il faut se créer un projet Windows 32 et ajouter les fichiers suivants dans le projet:
Même programme que ci-dessus, sauf en ce qui concerne l'arrêt, la fonction TerminateThread() n'a plus lieu d'être déclenchée, car la variable globale bFin va indiquer au thread quand il doit se terminer.
- L'utilisation de messages: nous verrons que, tout comme on peut envoyer des messages à une fenêtre, il est tout à fait possible d'envoyer un message à un thread. Celui-ci pourra alors lire sa file de message et terminer son code s'il s'agit d'un message de terminaison... (Voir le chapitre communication entre threads).
- L'utilisation d'évènements, nous pouvons envoyer un message, mais aussi un évènement... (Vois chapitre sur les évènements).

Lire le code de sorti d'un thread (pour gérer une éventuelle erreur)
GetExitCodeThread() récupère le paramètre de sortie d'un thread ou la valeur passée par la fonction TerminateThread().
BOOL GetExitCodeThread(
HANDLE hThread,
LPDWORD lpExitCode
);
Pour utiliser cette commande, il faut que le Thread soit THREAD_QUERY_INFORMATION au niveau de ses habilitations.
Les paramètres:
hThread Qui est le handle du thread
En sortie, lpExitCode pointe sur une variable qui recevra le code retour du
thread.
La fonction retourne une valeur à 0 si il y a une erreur.
Remarque : Si vous demandez le code de sortie du thread alors qu'il n'est pas terminé, vous recevrez STILL_ACTIVE (dont la valeur est 259). Il ne faut donc jamais avoir un thread qui retourne cette valeur, sinon impossible de savoir s'il s'agit d'une erreur ou si le thread est encore actif.
Dans vos applications Windows, il faut s'assurer que tous les threads sont bien arrêtés avant de mettre fin à l'application. Ce n'est pas le cas dans les exemples ci-dessus et pour cause, il s'agit d'utiliser une fonction comme entre autre: WaitForSignleObject() ou WaitForMultipleObjects() qui sera étudiées un peu plus tard.
Suspendre ou résumer un thread:
Une manière de suspendre un thread, nous l'avons vu précédemment, est de l'indiquer lors de la création d'un thread.
Mais on peut aussi vouloir le suspendre durant l'exécution de l'application. On utilisera alors la fonction SuspendThread()
DWORD SuspendThread(
HANDLE hThread // Handle du thread à suspendre
);
Attention : Au niveau sécurité et droit d'accès du thread, il faut évidemment avoir THREAD_SUSPEND_RESUME positionné
La fonction retourne la valeur du compteur de suspension avant incrémentation de celui par la fonction.
Attention, ce compteur ne peut pas dépasser une certaine valeur maximum (MAXIMUM_SUSPEND_COUNT). Si vous essayez, la fonction ne fera rien et retournera une erreur (-1).
Cette fonction ne devrait pas être utilisée. Au départ, elle était plus orientée debugger...
Pour redémarrer un thread suspendu, on utilisera ResumeThread():
La fonction ResumeThread décremente le compteur de suspension du thread. Si ce compteur arrice à zéro, le thread redémarre.
DWORD ResumeThread(
HANDLE hThread // Handle du thread à redémarrer
);
Attention : Au niveau sécurité et droit d'accès du thread, il faut évidemment avoir THREAD_SUSPEND_RESUME positionné
La fonction retourne en dword la valeur précédente du compteur de suspension du thread ou -1 si erreur
Si cette valeur est 0, le thread n'était pas suspendu. En gros, l'appel à la fonction n'a servi à rien !
Si cette valeur est 1, le thread était bien suspendu, et il vient de redémarrer.
Si cette valeur est > 0, le thread est toujours suspendu !
Toujours le même compteur qui s'incrémente, mais il y a deux nouvelles options pour suspendre ou résumer un thread.