Le premier site francophone dédié au développement Pocket PC


Multi-threading et synchronisation (partie 1)
 
   


Le concept du multi-threading

Windows CE est un systême d'exploitation (SE) multi-thread. A ne pas confondre avec multi-process. On dit d'un SE qu'il est multi-process lorsqu'il est capable de faire tourner plusieurs applications simultanément. On dit d'un SE qu'il est multi-thread lorsque non seulement il est capable d'exécuter plusieurs applications mais qu'au sein de chacune d'elles, il est capable d'exécuter simultanément plusieurs fonctions.

Je tiens toutefois à attirer l'attention sur le terme simultanément. En effet, jusqu'à preuve du contraire, un processeur n'est capable que de faire une seule chose à la fois. Le SE va donc s'arranger pour donner l'illusion que deux tâches s'exécutent parrallèlement. Pour ce faire il va partager le processeur entre tous les thread. Le temps est divisé en tranches infimes appellés quantum. L'algorithme qui détermine l'attribution des quantum aux threads est l'algorithme de scheduling. Ce dernier attribue le temps processeur en fonction des priorités des threads et des ressources détenues par eux. Le but de se document n'est pas de rentrer dans les détails du scheduler. Le passage d'un thread à un autre sur le processeur est appellé context switch ; Le context switching est une opération couteuse puisqu'elle consiste à sauvegarder l'état de la plupart des registres du processeur pour le thread sortant et de rétablir les valeurs des registres pour le thread entrant..

Dans la pratique, seuls les systèmes dotés de plusieurs processeurs peuvent physiquement exécuter plusieurs threads en parallèle : un par processeur en fait.

La plus petite entité d'exécution sous Windows CE est donc le thread. Chaque application en cours d'exécution en contient au minimum un : le thread primaire. Il est créé en même temps que le process et il exécute toujours le point d'entrée de l'application (WinMain ou main la plupart du temps). Lorsque le thread primaire se termine, l'application se termine.

Windows CE autorise un nombre maximum de 32 process simultanés. Il n'existe par contre pas de limite pour le nombre de thread si ce n'est une limite imposée par la memoire disponible sur l'appareil.

Créer un thread

A partir du thread primaire, il vous est loisible de lancer de nouveaux threads. La question qui vient tout de suite à l'esprit est : " Puis-je lancer n'importe laquelle des fonctions de mon application en thread ? " La réponse est non ! En effet une fonction thread possède un prototype imposé. Elle doit toujours être du type :

DWORD WINAPI MyThreadFunction (LPVOID p) ;

Pour créer un nouveau thread, on utilise l'API Win32 CreateThread dont voici le prototype :

HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId );

lpThreadAttributes est toujours NULL sous Windows CE puisqu'il n'y a pas de gestion de sécurité au niveau des objets du Kernel.
dwStackSize est la taille du stack à allouer pour cette fonction. Passez 0 pour une taille de stack par défaut
lpStartAddress est le nom de la fonction que vous désirez faire exécuter par le thread
lpParameter est une valeur qui sera passée au thread lors de sa création. Plus à ce sujet dans la suite de l'article.
dwCreationFlags permet de spécifier des drapeux de création pour le thread. Par exemple, il vous est possible de créer des threads don't l'exécution est directement mise en attente. Reportez vous au MSDN pour plus d'informations
lpThreadId est un paramètre de sortie qui contiendra l'ID du thread créé. Cet ID pourra vous servir par exemple pour changer sa priorité. Si vous n'êtes pas intéressé par sa valeur, vous pouvez passer NULL.

CreateThread retourne un handle sur le thread. Celui-ci pourra être utisé plus tard pour par exemple détecter si le thread en question est terminé.

La durée de vie d'un thread

Une fois que vous avez lancé votre thread, se pose le problème de sa longévité. Les règles sont simples :

Un thread se termine lorsqu'il sort de la fonction qu'il était chargé d'exécuter.
Un thread se termine lorsque le primary thread de l'application se termine.
Un thread peut aussi se terminer prématurément si il provoque une exception.

Ainsi donc si vous voulez garder un thread en vie durant toute la durée d'exécution de l'application, vous devrez nécessairement installer une boucle dans ce thread. Nous reviendrons sur ce point plus tard dans l'article.

A propos du Thread handle

Je vous ai signalé que la fonction CreateThread renvoyait un handle. Je vais vous démontrer une utilisation de ce handle.

Le handle d'un thread est dit signalé lorsque le thread s'arrête, il est non-signalé lorsqu'il est en cours d'exécution. Certaines fonctions Win32 de synchronisation comme WaitForSingleObject permettent d'attendre qu'un handle soit signalé. Je peux donc très facilement avec WaitForSingleObject attendre qu'un thread se termine. Cest le thème de mon premier exemple.

Lorsque vous n'avez plus besoin du handle, il est de bonne pratique de le libérer au moyen de l'API Win32 CloseHandle.

Exemple 1 : Lancement d'un thread et attente de sa fin.

#include "stdafx.h"               
DWORD WINAPI MyFirstThread(LPVOID p);
int WINAPI WinMain( HINSTANCE hInstance,
                 HINSTANCE hPrevInstance,
                 LPTSTR lpCmdLine,
                 int nCmdShow)
{
                 
   HANDLE hThread;

    hThread = CreateThread(NULL,0,MyFirstThread,NULL,0,NULL);
    if ( NULL == hThread)
         MessageBox(NULL,L"Echec de la céation du thread",L"Exemple 1",MB_OK);
    else
    {
       DWORD dwWait;

       dwWait = WaitForSingleObject(hThread,INFINITE);
       if (WAIT_OBJECT_0 == dwWait)
          MessageBox(NULL,L"Le thread est terminé",L"Exemple 1", MB_OK);
       
       CloseHandle(hThread);
       
     }
    return 0;
}
DWORD WINAPI MyFirstThread(LPVOID p)
{
   for(int i=1; i<= 10; i++)
      Sleep(500);

   return 0;
}

Le passage de paramètres à un thread

Pour passer des informations à un thread, on peut utiliser des variables globales. En effet, tous les threads tournant au sein d'une même application, utilisent le même espace d'adressage. Et donc à ce titre, un pointeur sur des données est valable pour tous ces threads. Il est cependant conseillé d'eviter d'abuser des variables globales : elles nuisent à la lisibilité et à la maintenance du code.

Une seconde approche consiste à passer un paramètre au thread via la fonction CreateThread. Dans ce cas vous êtes autorisé à passer un LPVOID au thread. Si vous avez juste une variable à passer au thread, passez directement cette dernière en paramètre en prenant soin d'effectuer un cast de son type vers LPVOID.
Si toutefois, vous devez passer un ensemble de valeurs à ce thread, vous pouvez créer une structure contenant toutes les informations et passer un pointeur sur cette structure. Voici un exemple de code utilisant cette technique.

typedef struct tagTHREAD_PARAMS
{
   int iID;
   int iMaxValue;              


} THREAD_PARAMS, *LPTHREAD_PARAMS;
int WINAPI WinMain( HINSTANCE hInstance,
                 HINSTANCE hPrevInstance,
                 LPTSTR lpCmdLine,
                 int nCmdShow)
 {
                 
     HANDLE hThread;
     LPTHREAD_PARAMS pParams;

     pParams = (LPTHREAD_PARAMS) malloc(sizeof(THREAD_PARAMS));
     pParams->iID = 123;
     pParams->iMaxValue = 2000;
     hThread = CreateThread(NULL,0,ReceptionThread,(LPVOID) pParams,0,NULL);

La fin d'exécution d'un thread

Globalement il y a deux manières d'arrêter un thread, la bonne et la mauvaise :) .

La mauvaise consiste à faire un appel à la fonction Win32 TerminateThread. Avec cette fonction, un thread peut en terminer un autre. Cette méthode bien que facile à utiliser n'est pas conseillée pour deux raisons :

Le thread visé va se terminer de manière brutale sans avoir l'occasion d'effectuer un clean-up, ce qui peut-être gênant dans certaines circonstances.

Si une ou plusieurs DLLs ont été utilisées par le thread visé, ces dernières ne recevront pas la notification DLL_THREAD_DETACH ou DLL_PROCESS_DETACH.

La seconde méthode (la bonne) consiste à faire parvenir une notification au thread que l'on désire arrêter afin de lui demander d'effectuer une sortie en douceur. Ceci n'est possible que si le thread est en mesure de vérifier de manière régulière qu'un tel message lui a été posté. L'exemple ci-dessous illustre mon propos. Dans cet exemple, un thread effectue de manière régulière une routine quelconque. A chaque exécution de sa routine, il est en mesure de détecter une demande d'arrêt de ses activités. La notification sera faite en utilisant un évènement manuel.

#include "stdafx.h"              

DWORD WINAPI ReceptionThread(LPVOID p);
int WINAPI WinMain( HINSTANCE hInstance,
                 HINSTANCE hPrevInstance,
                 LPTSTR lpCmdLine,
                 int nCmdShow)
{
                 
    HANDLE hThread;
    HANDLE hStopEvent;
    hStopEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
    hThread = CreateThread(NULL,0,ReceptionThread,(LPVOID) hStopEvent,0,NULL);
    if ( NULL == hThread)
       MessageBox(NULL,L"Echec de la céation du thread",L"Exemple 1",MB_OK);
    else
    {
       DWORD dwWait;

       MessageBox(NULL,L"Cliquez sur OK pour envoyer 
                         la demande de sortie 
                         au thread",L"Exemple 1", MB_OK);
       SetEvent(hStopEvent);
       dwWait = WaitForSingleObject(hThread,INFINITE);
       if (WAIT_OBJECT_0 == dwWait)
          MessageBox(NULL,L"Le thread est terminé",L"Exemple 1", MB_OK);
       CloseHandle(hThread);
    }
 return 0;


}
DWORD WINAPI ReceptionThread(LPVOID p)
{
    HANDLE hStopEvent = (HANDLE) p;
    DWORD dwWait;
    BOOL fRun = TRUE;


    while(fRun)
    {
       dwWait = WaitForSingleObject(hStopEvent,1000);
       switch(dwWait)
        {
          case WAIT_OBJECT_0:
           {
             // L'événement stop a été déclenché,
             // sortir proprement du thread
            fRun = FALSE;
           }
           break;

          case WAIT_TIMEOUT:
          {
            // Chaque seconde, effectuer le boulot
            DoSomething();
          }
          break;
     
      }
   }

  return 0;
}

Conclusion

Dans cette première partie, nous avons introduit la notion de thread. Nous avons couvert les aspects de création et de destruction d'un thread.

Dans une seconde partie, je m'attacherai à la synchronisation des threads. En effet, il est souvent le cas, que l'on doive agencer l'exécution des threads en fonction d'évènements extérieurs. Par exemple, un thread B ne commence à traiter les données que lorsque le thread A a terminé de les recevoir.

Suite au prochain numéro....

 

Laurent Docquir

 
       
   
 
   
Copyright 2001-2004 - Tous droits réservés
 
   

iPAQ est un produit de COMPAQ.
Visual Tools est un produit de Microsoft Corporation.
Toutes les autres marques et produits présents dans ces pages sont la propriété exclusive de leurs sociétés respectives.