Ajout de la sécurité des threads à une fonction de journalisation simple?

D’après ce que j’ai lu, les stream de sortie standard ne sont généralement pas thread-safe . J’ai une application C ++ (basée sur Windows, utilisant Visual Studio 2005) qui a une fonction de journalisation très simple:

void logText(ssortingng text) { if(g_OutputLogEnabled && g_OutputLog.is_open()) { ssortingng logDate = getDateStamp("%Y-%m-%d %H:%M:%S"); g_OutputLog << "[" << logDate << "]: " << text << endl; } cout << text << endl; // Also echo on stdout } 

Dans cet exemple, g_OutputLog est un ofstream et g_OutputLogEnabled est un booléen.

J’ai utilisé cette petite fonction dans mon application principale sans problème, mais je veux maintenant étendre son utilisation à certains fils enfants. Ces threads fonctionnent et impriment de manière asynchrone des données au fur et à mesure de leur exécution.

Question: Comment puis-je append une sécurité de thread simple au niveau de la ligne à cette routine? Tout ce qui m’intéresse vraiment, c’est que chaque ligne qui vient rest intacte dans mon journal. La performance n’est pas une préoccupation dans ce cas, mais (comme toujours) plus rapide est plus agréable.

Je sais que je pourrais utiliser des packages de journalisation tiers, mais je veux le faire moi-même pour que je puisse apprendre comment cela fonctionne . Ma connaissance multi-threading n’est pas ce qu’elle devrait être et j’essaie de l’améliorer.

J’ai entendu le terme sections critiques , et je suis un peu au courant des mutex et des sémaphores, mais que pourrais-je utiliser dans ce cas? Existe-t-il une solution propre et simple? Merci d’avance pour tout conseil.

Utilisez un verrou de scope tel que:

 void logText(ssortingng text) { if(g_OutputLogEnabled && g_OutputLog.is_open()) { ssortingng logDate = getDateStamp("%Y-%m-%d %H:%M:%S"); boost::scoped_lock (g_log_mutex); //lock g_OutputLog << "[" << logDate << "]: " << text << endl; } //mutex is released automatically here boost::scoped_lock (g_cout_log_mutex); //lock on different mutex! cout << text << endl; // Also echo on stdout } 

Ou vous pouvez utiliser std::unique_lock si votre compilateur le supporte.


Comment implémenteriez-vous scoped_lock si vous ne pouvez pas utiliser Boost et si vous n'avez pas std::unique_lock ?

Définissez d'abord la classe de mutex :

 #include  class mutex : private CRITICAL_SECTION //inherit privately! { public: mutex() { ::InitializeCriticalSection(this); } ~mutex() { ::DeleteCriticalSection(this); } private: friend class scoped_lock; //make scoped_lock a friend of mutex //disable copy-semantic mutex(mutex const &); //do not define it! void operator=(mutex const &); //do not define it! void lock() { ::EnterCriticalSection(this); } void unlock() { ::LeaveCriticalSection(this); } }; 

Ensuite, définissez scoped_lock comme scoped_lock :

 class scoped_lock { mutex & m_mutex; public: scoped_lock(mutex & m) : m_mutex(m) { m_mutex.lock(); } ~scoped_lock() { m_mutex.unlock(); } }; 

Maintenant, vous pouvez les utiliser.

Étant donné que la fonction n’est clairement pas axée sur les performances, ajoutez un simple mutex et verrouillez-le avant d’écrire dans les stream.

Bien sûr, pour des raisons de sécurité, le verrou doit être automatiquement libéré, alors assurez-vous d’utiliser RAII.

Je recommande un regard sur la bibliothèque Boost.Threads.

Oui, vous devez append une sorte de protection à votre code. Vous n’avez besoin de rien d’exotique, vous voulez accéder à une ressource (votre stream) alors qu’elle est utilisée par autre chose. Deux types d’objects de synchronisation fonctionnent bien avec cette tâche: les sections critiques et Mutex. Pour plus de détails sur les verrous, vous pouvez commencer à lire cet article sur Wikipedia . Mutex sont généralement plus lents mais peuvent être partagés entre les processus, ce n’est pas votre cas. Vous pouvez donc utiliser une section critique simple pour synchroniser vos threads.

Peu de conseils si vous ne prévoyez pas d’utiliser une bibliothèque en 3ème partie (comme le grand Boost).

Lorsque la fonction EnterCriticalSection permet d’acquérir le verrou. Si la ressource est verrouillée par quelqu’un d’autre, votre thread sera suspendu et réactivé lorsque la ressource sera libérée par son propriétaire (de plus, votre thread verrouillé pourrait avoir une priorité). Pour les verrous à durée de vie courte, cela ne constitue peut-être pas une solution optimale, car la suspension / reprise d’un thread nécessite beaucoup de temps et de ressources. Pour cette raison, vous pouvez définir un spin ; Avant de suspendre votre thread, le système d’exploitation consumra un peu de temps en ne faisant rien sur ce thread, cela peut donner le temps de verrouiller le propriétaire pour terminer son travail et libérer le contenu. Pour l’utiliser, vous devez initialiser votre section critique avec InitializeCriticalSectionAndSpinCount au lieu de la section InitializeCriticalSection .

Si vous envisagez d’utiliser une section critique que vous pouvez considérer pour envelopper tout ce dont vous avez besoin dans une classe, vous utiliserez l’étendue des variables pour tout faire et votre code sera plus clair (ceci n’est qu’un exemple, une véritable implémentation ne peut pas être aussi naïve) :

 class critical_section { public: critical_section() { // Here you may use InitializeCriticalSectionAndSpinCount InitializeCriticalSection(&_cs); // You may not need this behavior, anyway here when you create // the object you acquire the lock too EnterCriticalSection(&_cs); } ~critical_section() { LeaveCriticalSection(&_cs); DeleteCriticalSection(&cs); } private: CRITICAL_SECTION _cs; }; 

Sous Unix / Linux (ou vous voulez être portable), vous devez utiliser les fonctions pthread.h pour Mutex.

Lock peut ne pas suffire

Ce n’est que la première étape, si votre application se connecte beaucoup, vous pouvez ralentir tous les threads en attente de journal (ne faites rien a priori, vérifiez et profilez). Si tel est le cas, vous devez créer une queue, votre fonction logText() simplement un nouvel élément (pré-formaté) dans la queue et appelle PulseEvent pour signaler un événement (créé avec CreateEvent ). Vous aurez un deuxième thread en attente de cet événement avec WaitForSingleObject , votre thread se réveillera et il sortira un élément de la queue. Vous avez peut-être encore besoin d’un mécanisme de locking (ou vous pouvez écrire votre propre file d’ attente concurrente non bloquante pour éviter tout locking. Cette solution est plus rapide et n’utilise aucun verrou, mais je pense que vous ne devriez faire cela que si vous souhaitez étudier le sujet (threads), généralement pour une simple exigence de journal, vous n’avez pas besoin d’append une telle complexité.