Communication inter-thread

 

 

Un première méthode pour permettre de faire communiquer deux threads d'un même processus, consiste à déclarer des variables dans le Heap (le tas en français) de notre programme. En effet, l'espace d'adressage sera alors identique pour ces deux threads. Cependant, il faudra alors bien faire attention lors d'une mise à jour de ces variables à ne pas écraser des données de l'autre thread. On pourra alors utiliser des variables spécifiques à un thread pour l'écriture. L'autre thread ne faisant que de la lecture dans ces variables. Ou encore utiliser des fonctions de synchronisation comme les mutex (voir chapitre un peu plus loin), s'il s'avère indispensable d'utiliser la même variable pour ces deux threads en écriture.

 

Le cas étudié ici est celui-ci: chaque thread a ses variables définies et ce n'est pas dans le Heap (ou TAS). Par conséquent, le second thread ne peut pas y accéder, car il ne s'agit pas du même espace d'adressage.

Pour résoudre ce problème, on utilisera alors les fonctions de gestion des messages:

Les threads pourront donc envoyer des messages:

Deux cas:

  • Le thread va envoyer un message à une fenêtre. En fait, la fenêtre a forcement un thread associé pour gérer les messages (et plus...).
  • On utilisera alors les commandes standards:

    • SendMessage() : Pour envoyer un message vers une fenêtre. Le thread émetteur sera alors en attente jusqu'à ce que la fenêtre ai traité ce message.
    • PostMessage() : Idem que ci-dessus, sauf que le thread n'attendra pas le traitement de son message par la fenêtre.
  • Le thread va envoyer un message à un autre thread.

    On utilisera alors la commande standard:

    • PostThreadMessage () : le thread n'attendra pas le traitement de son message par le
      thread récepteur...

C'est bien beau de les envoyer, mais il faut aussi pouvoir les lire:

  • Evidemment, il peut s'agir d'une fenêtre:

    On utilisera alors les commandes standards:

    • GetMessage() : Lecture des messages avec attente si pas de message dans la file. Cette fonction est intéressante car elle va mettre en sommeil le thread associé s'il n'y a pas de message à traiter.
    • PeekMessage() : Lecture de messages sans attente.
  • Ou alors d'un thread. Dans ce cas, on utilisera les mêmes fonctions que ci-dessus.

 

Attention: On utilisera généralement GetMessage() pour une fenêtre plutôt que PeekMessage(). Ainsi, lorsque les messages de la fenêtre seront tous traités, le thread partira faire dodo. Libérant ainsi du temps CPU.

A l'inverse, un thread qui n'est pas rattaché à une fenêtre doit généralement faire des traitements qui peuvent être long ou non: le thread a du boulot à faire qui est bien plus important que de traiter des messages. La lecture d'un message servant plus généralement à interrompre son traitement. Dans ce cas, on évitera de le faire partir en sommeil avec un GetMessage() et on utilisera plutôt PeekMessage (). Cependant, il faudra voir aussi à limiter le nombre d'appel à la fonction PeekMessage() (inutile de lire la file de message à chaque itération, d'autant plus vrai si le temps de traitement d'une itération est très court).

Détail des fonctions:

Déjà étudié : GetMessage() et PeekMessage().

SendMessage() et PostMessage(): Pour envoyer un message à une fenêtre.

La fonction PostThreadMessage(): Pour envoyer un message à un autre thread.

Envoyer une message sans attendre la réception (Equivaut à ne pas avoir un accusé réception).

BOOL PostThreadMessage(

DWORD idThread,
UINT Msg,
WPARAM wParam,
LPARAM lParam

);

Avec comme paramètres:

idThread: Identifiant d'un thread qui recevra le message. ATTENTION: identifiant et pas le handle !

Msg : Type de message à envoyer (utilisez WM_USER ou WM_APP par exemple).
wParam : Information complémentaire
lParam : Encore une information complémentaire sur le message

En sortie, la fonction retourne 0 si problème. Il faudra alors vérifier le type de problème avec GetLastError. Si ERROR_INVALID_THREAD_ID cela signifie que l'identifiant du thread est invalide, sinon, il n'existe pas encore de file de message dans le thread. (voir remarques importantes ci-dessous).

La fonction retourne une valeur > 0 si pas de problème lors de l'émission.


Remarques importantes:

La file de message d'un thread n'est pas créée tant qu'une fonction comme PeekMessage() ou GetMessage() n'est pas déclenchée dans ce thread ou au moins dans son processus. Cela signifie donc que le thread qui veut envoyer une message par PostThreadMessage doit vérifier que la fonction n'a pas échouée du fait de l'absence de cette file de message dans le thread destinataire. Voir éventuellement à faire une synchronisation entre les deux threads (vue un peu plus loin), le thread censé recevoir des messages effectuant d'abord un PeekMessage() et informant ensuite le Thread émetteur qu'à partir de maintenant, il pourra recevoir des messages.

Voici un exemple pour forcer la création d'une file de message:PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE)

Si une erreur se produit durant l'envoi d'un message (par un PostThreadMessage() par exemple), il est inutile de refaire immédiatement un PostThreadMessage(). Car si vous avez bien compris le fonctionnement des threads, 1 seul thread tourne à la fois. Plutôt que de consommer tout notre quotas temps à refaire des envois de message qui n'aboutiront pas, il est préférable de rendre immédiatement la main aux autres threads par un simple appel à Sleep (Sleep (0)), pour permettre aux autres threads de s'initialiser. Lorsque le sheduler nous donnera de nouveau la main, nous pourront refaire un appel à la fonction pour envoyer un message.

Voici un exemple qui illustre notre cas:

Image non trouvée !Attention, il faut créer ici un projet Win32 console. En effet, si nous faisions un programme Windows "traditionnel", le Thread Principal générerait une file de message (pour traiter les messages de notre fenêtre). Par conséquent et par héritage, nos threads pourraient traiter tout de suite les messages.

Ce que fait cet exemple : Création de deux threads. Le premier va envoyer tout de suite des messages au second. Seulement voilà, le second thread est long à s'initialiser. Sa file de message ne sera créé qu'au bout de 5 secondes. De plus, il terminera complétement son initialisation que 2.5 secondes après la création de sa file de message. C'est à ce moment qu'il pourra traiter les messages reçus. Cette speudo phase d'initialisation étant remplacée ici par des appelles à la fonction Sleep(). Les différents threads indiquent alors ce qu'ils font et ce qui se passe... Le programme s'arrête au bout de 13 secondes...Comme nous le verrons plus tard, la terminaison du programme est plutôt violente. De même, les deux threads utilisent la même ressource pour l'affichage (càd l'écran). Il devrait donc y avoir des synchronisations entre eux pour pouvoir utiliser notre écran et éviter des conflits...

Une autre solution pour vérifier qu'un thread est prêt à traiter des messages est l'utilisation d'évènement. On utilise alors WaitForSingleObject (Etudié un peu plus tard) sur cet évènement. Le second thread ne devant traiter l'évènement qu'après avoir créer la file de message (pour plus de détail, voir le chapitre correspondant).