Le code qui suit est vieux et n'est pas adapté aux réalités de la programmation depuis l'avènement de C++ 11.
Il arrive qu'on veuille rédiger un programme dans lequel coexistent plusieurs threads, et qu'on veuille qu'un thread donné ne pose une action que si un autre thread lui en a donné le signal. Pensons par exemple à un système où le traitement et l'affichage sont deux tâches séparées, où l'affichage doit être rafraîchi fréquemment, et où on ne veut pas que le thread d'affichage n'opère quelque traitement que ce soit si le thread de traitement n'en signale pas le besoin.
Ce type d'interaction se modélise avantageusement avec des événements. Le mot événement doit être pris ici au sens de l'outil de synchronisation du même nom, pas au sens d'une méthode rappelée par le thread donnant le signal.
Un exemple suit (écrit avec la notation de Microsoft Windows)...
#include <windows.h>
unsigned long __stdcall ThreadA(void *);
unsigned long __stdcall ThreadB void *);
#include <cassert>
class Sync
{
HANDLE evenement_;
bool fin_;
public:
// on aurait pu utiliser un événement « auto reset »
Sync()
: fin_{}, evenement_{CreateEvent(0, FALSE, FALSE, 0)}
{
assert(evenement_ != ERROR_INVALID_HANDLE);
}
// on pourrait utiliser un mutex commun dans attendre_evenement()
// et dans provoquer_evenement()
void attendre_evenement() const
{
WaitForSingleObject(evenement_, INFINITE);
ResetEvent(evenement_);
}
void provoquer_evenement() const
{ SetEvent(evenement_); }
bool est_termine() const noexcept
{ return fin_; }
void terminer() noexcept
{ fin_ = true; }
~Sync() noexcept
{ CloseHandle(evenement_); }
};
#include <iostream>
int main()
{
using std::cin;
Sync sync;
HANDLE hTh[] =
{
CreateThread(0, 0, ThreadA, &sync, 0, 0),
CreateThread(0, 0, ThreadB, &sync, 0, 0)
};
enum { N = sizeof(hTh)/sizeof(*hTh) };
// lire une touche avant de terminer
char c;
cin >> c;
sync.terminer ();
WaitForMultipleObjects(hTh, N, INFINITE, TRUE);
}
Thread A | Thread B |
---|---|
|
|
Un problème de cet exemple est qu'il exploite une mécanique non portable (celle des événements de la plateforme Microsoft Windows). En soi, ce n'est pas dramatique, mais on ne pourrait pas nécessairement le transposer à d'autres plateformes sans implémenter manuellement une équivalent local de cette mécanique (voir ici pour des exemples). De même, un produit comme Java qui se veut portable à toutes les plateformes pourrait vouloir spécifiquement éviter de se lier à des outils trop locaux.
Une stratégie plus portable pour en arriver à un effet semblable serait d'utiliser une variable pour représenter la demande de réaffichage du thread A au thread B. Par contre, on tend trop facilement à faire une erreur un peu bête, qui est d'utiliser un booléen pour assurer le passage d'un signal.
Ceci fonctionne dans les cas simples :
#include <windows.h>
unsigned long __stdcall ThreadA (void *);
unsigned long __stdcall ThreadB (void *);
class Sync
{
bool signal_;
bool fin_;
public:
Sync() noexcept
: fin_{}, signal_{}
{
}
// on pourrait utiliser un mutex commun dans attendre_evenement()
// et dans provoquer_evenement()
void attendre_evenement() const noexcept
{
while(!signal_) Sleep(0);
signal_ = false;
}
void provoquer_evenement() const noexcept
{ signal_ = true; }
bool est_termine() const noexcept
{ return fin_; }
void terminer() noexcept
{ fin_ = true; }
};
#include <iostream>
using std::cin;
int main ()
{
Sync sync;
HANDLE hTh[] =
{
CreateThread (0, 0, ThreadA, &sync, 0, 0),
CreateThread (0, 0, ThreadB, &sync, 0, 0)
};
enum { NB_THREADS = sizeof(hTh)/sizeof(*hTh) };
// lire une touche avant de terminer
char c;
cin >> c;
sync.terminer();
WaitForMultipleObjects(hTh, NB_THREADS, INFINITE, TRUE);
}
Thread A | Thread B |
---|---|
|
|
Cette stratégie est plus gourmande en temps de traitement que celle utilisant un événement, du fait qu'elle fait une boucle qui suspend le thread appelant pour attendre l'occurrence d'un événement (avec un événement pris en charge par le système d'exploitation, le thread serait véritablement suspendu). Cependant, puisque les événements couverts plus haut impliquent des invocations de services du système d'exploitation, leur utilisation est coûteuse. Si la contention est faible entre les threads, une attente active comme celle ci-dessus donnera parfois de meilleurs résultats qu'une suspension volontaire.
En retour, on pourrait permettre au thread mis en attente d'un événement de spécifier la fréquence à laquelle la vérification serait faite, ce qui permettrait à un thread de consommer aussi peu de temps de traitement que possible. à titre d'exemple, si on estime que 20 rafraîchissements par seconde suffit, alors on peut dormir 1000/20 millisecondes dans la boucle insérée à même la méthode attendre_evenement(), ce qui est beaucoup moins coûteux que dormir 0 millisecondes.
Cette solution, de même que celle exploitant des événements pris en charge par le système d'exploitation, comporte des risques de synchronisation importants, surtout s'il est possible que plusieurs threads attendent le même événement : lequel des threads sera alors responsable de réinitialiser l'événement (ou de remettre le booléen à faux)? Se pourrait-il qu'un événement se produise pendant le sommeil d'un thread et que ce thread le manque parce qu'un autre a reconnu l'événement et l'a déjà réinitialisé?
La racine du problème est double, et tient du caractère binaire (vrai/ faux) d'une valeur booléenne et du fait que la tâche de réinitialiser l'événement ne peut être donné à l'un ou l'autre des threads, du moins pas dans le cas général. La solution au problème repose sur l'analyse de ces deux constats :
Voici donc l'idée :
L'illustration programmée de cette idée suit.
#include <windows.h>
unsigned long __stdcall ThreadA(void *);
unsigned long __stdcall ThreadB(void *);
class Sync
{
int nb_evenements_;
bool fin_;
public:
Sync() noexcept
: fin_{}, nb_evenements_{}
{
}
// On change de philosophie ici, remarquez bien
int nb_evenements() const noexcept
{ return nb_evenements_; }
}
void provoquer_evenement() noexcept
{ ++nb_evenements_; }
bool est_termine() const noexcept
{ return fin_; }
void terminer() noexcept
{ fin_ = true; }
};
#include <iostream>
using std::cin;
int main()
{
Sync sync;
HANDLE hTh[] =
{
CreateThread(0, 0, ThreadA, &sync, 0, 0),
CreateThread(0, 0, ThreadB, &sync, 0, 0)
};
enum { N = sizeof(hTh)/sizeof(*hTh) };
// lire une touche avant de terminer
char c;
cin >> c;
sync.terminer();
WaitForMultipleObjects(hTh, N, INFINITE, TRUE);
}
Thread A | Thread B |
---|---|
|
|
Remarquez que cette solution a l'élégance d'un découplage plus strict de la gestion de l'événement, et s'adapte beaucoup mieux à une stratégie où plusieurs threads consultent l'événement en question. Elle a aussi l'avantage non négligeable de ne pas nécessiter une rustine (une patch) à la fin du thread A pour débloquer le thread B s'il est en attente d'un événement (et peut donc économiser un affichage).