Quelques raccourcis :
La multiprogrammation offre son lot de périls et d'opportunités d'erreurs comme d'horreurs...
Cette horreur est de moi (je suis le coupable), et la solution au problème posé par cet article a été trouvée par John Serri, étudiant au DGL de l'Université de Sherbrooke et qui a suivi IFT756 dans ma classe à l'hiver 2006. L'idée ici est qu'on cherche trop souvent au mauvais endroit et qu'un peu de rigueur pour les vérifications de base sauverait souvent beaucoup de temps.
Notez tout de suite que le code de cette entrée est non-portable et ne profite pas des outils (nettement préférables) de C++ 11. Je vous en prie, si vous avez accès à un compilateur à jour, écrivez du code portable et profitez de std::thread, std::mutex et autres!
J'avais écrit un petit programme de démonstration pour le problème suivant: lancer plusieurs threads tous désireux d'écrire un message (du type Je suis le thread n où n est un entier positif différent pour chaque thread) sur un même flux et assurer l'écriture synchronisée des messages. Ce type de programme est simple à rédiger et demande habituellement d'écrire un mécanisme (disons une petite classe) faisant en sorte que toutes les écritures sur le flux visé passent par elle et assurant, de manière encapsulée, l'obtention et le relâchement d'un outil de synchronisation (un mutex).
Ma propre version du programme allait comme suit. Si vous la trouvez complexe (ce qui est possible), c'est probablement parce qu'elle exploite des stratégies trop peu connues (générateurs, foncteurs, algorithmes standards, etc.) mais il n'y a vraiment pas grand-chose là-dedans :
unsigned long __stdcall Gosseur(void *);
#include <windows.h> // beurk
#include <numeric>
template <class It>
class CreerThread
{
unsigned long (__stdcall *thread_)( void*);
It src;
public :
CreerThread(unsigned long (__stdcall *p)(void*), It src)
: thread_(p), src_{src}
{
}
HANDLE operator()()
{
HANDLE h = CreateThread(0, 0, thread_, &(*src), 0, 0);
++src;
return h;
}
};
template <>
class CreerThread<void*>
{
unsigned long (__stdcall *thread_)(void*);
public :
CreerThread(unsigned long (__stdcall *p)(void*))
: thread_{p}
{
}
HANDLE operator()()
{ return CreateThread (0, 0, thread_, 0, 0, 0); }
};
//
// Bof
//
#include "Incopiable.h"
class Mutex
: Incopiable
{
HANDLE m;
public :
Mutex()
: m{CreateMutex(0, FALSE, 0)}
{
}
~Mutex()
{ CloseHandle(m); }
void obtenir() const volatile noexcept
{ WaitForSingleObject(m, INFINITE); }
void relacher() const volatile noexcept
{ ReleaseMutex(m); }
};
class Autoverrou
{
const volatile Mutex &m;
public :
Autoverrou(const volatile Mutex &m) noexcept
: m{ m }
{
m.obtenir();
}
~Autoverrou()
{ m.relacher(); }
};
#include <ostream>
class FluxSynchro
{
Mutex m;
std::ostream &os;
public :
FluxSynchro(std::ostream &os)
: os{ os }
{
}
template <class T>
volatile FluxSynchro& operator<<(const T &val) volatile
{
Autoverrou av{ m };
os << val;
return *this ;
}
void finirflush()
{ os.flush(); }
};
#include <fstream>
#include <iostream>
namespace
{
std::ofstream ofs{ "out.txt" };
//FluxSynchro sync_cout{ std::cout };
//FluxSynchro sync_cerr{ std::cerr };
FluxSynchro sync_fich{ ofs };
}
#include <algorithm>
int main()
{
using namespace std;
const int NTHREADS = 100;
unsigned int params[NTHREADS];
iota(begin(params), end(params), 0u);
HANDLE hTh [NTHREADS] = { 0 };
generate_n(hTh, NTHREADS, CreerThread <unsigned int*>(Gosseur, params));
WaitForMultipleObjects(NTHREADS, hTh, TRUE, INFINITE);
sync_fich.finirflush();
for_each(begin(hTh), end(hTh), CloseHandle);
}
#include <string>
#include <sstream>
unsigned long __stdcall Gosseur(void *p)
{
using std::stringstream;
stringstream sstr;
sstr << "Gosseur numéro " << * reinterpret_cast <Sequence<unsigned int>::value_type*>(p) << '\n';
sync_fich << sstr.str();
return {};
}
Mon problème était que tout semblait se terminer normalement, sans erreur de synchronisation, mais que certains messages n'étaient pas écrits sur le flux en fin de programme. Parfois j'en avais 24, parfois j'en avais 84, parfois j'en avais 98... Un problème aléatoire d'écritures synchronisées mais incomplètes.
L'horreur : trop présumer, sans assez vérifier. Le code semblait impeccable, se comporter correctement, les threads étaient manifestement tous complétés correctement puisque l'opération WaitForMultipleObjects() avec valeur INFINITE en tant que délai était réalisée avant que la répétitive procédant à la fermeture des HANDLE des threads ne soit réalisée. J'ai donc pensé que l'opération vidant le flux (le flush()) n'était pas réalisée de manière synchrone (sens faible), et j'ai mis mon problème sur le dos d'une incompréhension chez moi du sens de l'opération flush().
Plusieurs amis et collègues ont été comme moi intrigués par ce comportement. J'ai exploré de fond en comble les entrées/ sorties standard sans rien trouver de propre à mon problème (mais, comme pour tout problème foncièrement fécond, j'ai trouvé beaucoup d'information extrêmement pertinente à d'autres problèmes à travers mes lectures), et je commençais à pencher du côté d'un problème d'implémentation propre à la plateforme. Ayant été mis face à face avec le sync daemon de AIX 4.1 dans le passé, je savais que l'écriture effective sur un média est une chose susceptible d'être faite de manière asynchrone à l'action d'un processus, mais je n'avais pas rencontré de cas où l'écriture s'avérait parfois incomplète (hormis des situations pathologiques où le média de destination était plein).
La clé du bonheur : examiner les codes d'erreur de nos fonctions avant d'aller chercher plus loin. C'est ici que l'ami John (voir plus haut) a eu la bonne idée d'aller récupérer le code retourné par WaitForMultipleObjects(). Je lui donne la parole :
« Il y a un grand professeur qui a dit qu'il était important de vérifier les codes de retour. Par exemple WaitForMultipleObjects(). En effet, dans ton code, cette fonction retourne WAIT_FAILED. Un appel à GetLastError() retourne 87 soit "The parameter is incorrect".
Si on regarde dans l'aide de WaitForMultipleObjects(), on voit : Parameters: nCount [in] Number of object handles in the array pointed to by lpHandles. The maximum number of object handles is MAXIMUM_WAIT_OBJECTS. Si on regarde la valeur de MAXIMUM_WAIT_OBJECTS, on constate que c'est 64.
Si tu baisses ton nombre de threads à 64, alors ça fonctionne bien. Oui bien il faut faire plusieurs WaitForMultipleObjects().
Bizous! »
Et pan! Dans les dents! Mais ce fut instructif.