Threads portables

Écrire des mutex portables et des événements portables est une chose relativement simple. La technique s'enseigne bien et s'inspire de l'idiome pImpl (pour Private Implementation) bien connu des programmeuses et des programmeurs C++.

Pour qui veut des objets pleinement portables, cependant, le cas des threads et des objets autonomes est plus délicat du fait que l'objet autonome, pour bien faire son travail d'abstraction du concept d'action autonome, doit prendre en charge le fonctionnement du thread : son lancement, son arrêt, sa rythmique, la tâche à réaliser de manière autonome, et ainsi de suite.

On s'attend donc à ce que Autonome soit une classe à part entière (bien qu'abstraite à cause de la méthode agir() qui différera d'un dérivé de Autonome à l'autre), et contienne tous les attributs requis pour remplir son mandat. Cela dit, on ne veut pas voir le mot HANDLE (pour prendre la dénomination Win32) apparaître dans la déclaration de la classe Autonome car cela contreviendrait à notre démarche d'abstraction de la plateforme et de portabilité pleine et entière.

Pour clarifier la démarche suivi ici, le discours proposé sera découpé comme suit :

Toute classe dont les instances auront à être autonomes n'aura plus qu'à dériver de Autonome, à définir le sens de la méthode agir() et à fixer le rythme d'itération à travers les mécanismes offerts par Rythmique.

Abstraction d'une action indépendante – Acteur

L'abstraction la plus simple est sûrement Acteur :

Acteur.h
#ifndef ACTEUR_H
#define ACTEUR_H
struct Acteur
{
   virtual void agir() = 0;
   virtual ~Acteur() = default;
};
#endif

Abstraction d'un rythme calibré – Rythmique

Sans être très complexe, Rythmique a ceci de particulier qu'elle doit cacher la fonction mandatée de suspendre l'exécution du thread courant puisque ce type de fonctionnalité est fondamentalement non portable.

Rythmique.h Rythmique.cpp
#ifndef RYTHMIQUE_H
#define RYTHMIQUE_H
class Rythmique
{
public:
   using delai_t = long;
private:
   delai_t delai_;
public:
   Rythmique(delai_t delai = {}) noexcept
      : delai_{delai}
   {
   }
   delai_t delai() const volatile noexcept
      { return delai_; }
   void delai(delai_t delai) volatile noexcept
      { delai_ = delai; }
   void suspendre() const volatile;
};
#endif
#include "Rythmique.h"
#include <windows.h>
void Rythmique::suspendre() const volatile
   { Sleep(delai()); }

Abstraction de l'idée d'être arrêtableStoppable

La classe Evenement est laissée en exercice. Si vous voulez des indices susceptibles de vous aider à bien la rédiger, voir la classe Mutex.

Toujours sans qu'elle ne soit véritablement complexe, l'abstraction Stoppable repose sur l'abstraction Evenement du fait qu'il est (beaucoup!) plus efficace de suspendre un thread en attente d'un événement que de le faire itérer bêtement jusqu'à ce qu'une variable change d'état.

Dans le code qui suit, les attributs booléens devraient être des atomic<bool> pour que le code soit portable.

Stoppable.h
#ifndef STOPPABLE_H
#define STOPPABLE_H
//
// IEvenement et Evenement sont des exercices
// de rédaction de code dans mes cours
//
#include "Evenement.h"
class Stoppable
{
   bool arret_demande_,
        arret_complete_;
   Evenement evenement_;
public:
   Stoppable()
      : arret_demande_{false}, arret_complete_{false}
   {
   }
   bool arret_complete() const volatile noexcept
      { return arret_complete_; }
   bool arret_demande() const volatile noexcept
      { return arret_demande_; }
   void demander_arret() volatile noexcept
      { arret_demande_ = true; }
   void arret_complet() volatile noexcept
   {
      arret_complete_ = true;
      evenement_.Provoquer();
   }
   bool attendre_arret(const int delai = IEvenement::INFINI)
      const volatile
   {
      return evenement_.attendre(delai);
   }
};
#endif

Notez qu'il pourrait être pertinent d'extraire le type utilisé pour représenter un rythme d'itération dans Rythmique pour faire en sorte que ce type soit le même que celui utilisé ici pour le délai d'attente pour la fin de l'exécution d'un thread.

Abstraction d'existence concurrente – Autonome

Le véritable travail délicat se trouve au niveau de la classe Autonome. En effet :

Le fichier d'en-tête suit :

#ifndef AUTONOME_H
#define AUTONOME_H
#include "Acteur.h"
#include "Rythmique.h"
#include "Stoppable.h"
class ThreadRep;
class Autonome
   : public Acteur, public Rythmique, public Stoppable
{
   ThreadRep *thread_;
protected:
   Autonome(delai_t delai = {});
   bool demarrer();
   void arreter() noexcept;
public:
   virtual ~Autonome() noexcept
      { arreter(); }
};
#endif

Le fichier source de Autonome définira :

Le code suit. Remarquez que le code démarrant un thread générique le crée en mode suspendu, pour être en mesure d'initialiser correctement le ThreadRep qui lui sera associé (et d'éviter ainsi de potentielles conditions de course), puis démarré une fois le tout prêt à être utilisé.

Remarquez aussi que la fonction ze_thread() a été placée à l'intérieur d'un espace nommé anonyme dans le but de ne pas la faire apparaître à l'édition des liens – il s'agit d'une fonction destinée à un usage interne seulement.

#include "Autonome.h"
#include <windows.h>
namespace
{
   unsigned long __stdcall ze_thread(void *p)
   {
      Autonome &a = *static_cast<Autonome *>(p);
      while (!a.arret_demande())
      {
         a.agir();
         a.suspendre();
      }
      a.arret_complet();
      return {};
   }
}

class ThreadRep
{
   HANDLE h;
public:
   ThreadRep(HANDLE h) noexcept
      : h{h}
   {
   }
   ~ThreadRep() noexcept
      { CloseHandle(h); }
};

Autonome::Autonome(delai_t delai)
   : Rythmique{delai}, thread_{}
{
}

bool Autonome::demarrer()
{
   HANDLE h = CreateThread (0, 0, ::ze_thread, this, CREATE_SUSPENDED, 0);
   if (h == INVALID_HANDLE_VALUE) return false;
   try
   {
      thread_ = new ThreadRep(h);
   }
   catch(...)
   {
      CloseHandle(h);
      return false;
   }
   ResumeThread(h);
   return true;
}

void Autonome::arreter() noexcept
{
   demander_arret();
   attendre_arret();
   delete thread_;
}

On pourrait imaginer des compléments à cette base de travail mais elle nous suffira ici (comme elle suffirait pour une grande majorité d'applications multiprogrammées).

Pour raffiner ce qui est proposé ici, je vous invite à faire de la classe Autonome une classe Incopiable, à faire de ThreadRep une classe interne de la classe Autonome, et à remplacer le pointeur brut sur un ThreadRep par un pointeur intelligent (un std::unique_ptr de préférence).

Rendre le tout plus robuste

Le brillant Alexandre Richard, étudiant au Collège Lionel-Groulx en informatique industrielle m'a écrit, à la session A-2006, un petit mot plein de sens : le code proposé ci-dessus n'est pas suffisamment robuste pour être utilisé de manière commerciale.

En effet, la méthode d'instance arreter() de la classe Autonome ne réinitialise pas son attribut de type ThreadRep* à nullptr. Bien que cela ne pose pas de problème réel dans le contexte simplifié où la classe Autonome est présentée ici, cela rend périlleux l'implémentation d'un programme qui arrêterait puis redémarrerait le thread ou qui arrêterait le thread avant invocation du destructeur.

Voici donc une version plus robuste de la classe Autonome. Notez qu'elle demeure perfectible (entre autres, il faudrait éviter les conditions de course dans demarrer() et dans arreter()) mais je vous laisse le soin de mettre en place les mécanismes requis pour votre code (je ne veux pas ralentir une version générale).

#include "Autonome.h"
#include <windows.h>
namespace
{
   unsigned long __stdcall ze_thread(void *p)
   {
      Autonome &a = *static_cast<Autonome *>(p);
      while (!a.arret_demande())
      {
         a.agir();
         a.suspendre();
      }
      a.arret_complet();
      return {};
   }
}

class ThreadRep
{
   HANDLE h;
public:
   ThreadRep(HANDLE h) noexcept
      : h{h}
   {
   }
   ~ThreadRep() noexcept
      { CloseHandle(h); }
};

Autonome::Autonome(delai_t delai)
   : Rythmique{delai}, thread_{}
{
}

bool Autonome::demarrer()
{
   // pour éviter les threads sauvages
   if (thread_) return false;
   HANDLE h = CreateThread(0, 0, ::ze_thread, this, CREATE_SUSPENDED, 0);
   if (h == INVALID_HANDLE_VALUE) return false;
   try
      { thread_ = new ThreadRep(h); }
   catch(...)
   {
      CloseHandle(h);
      return false;
   }
   ResumeThread(h);
   return true;
}

void Autonome::arreter() noexcept
{
   if (thread_)
   {
      demander_arret();
      attendre_arret();
      delete thread_;
      // au cas où arreter() ne serait pas invoqué par Autonome::~Autonome()
      thread_= nullptr;
   }
}

Valid XHTML 1.0 Transitional

CSS Valide !