M4104C-TP-Boost-Exercice6

Exercice donné en test le 06/04/2007

Dans le cours magistral, une implémentation de Boîte Aux Lettres (BAL) a été donnée en utilisant un mutex et une variable condition.

Il a été montré que cette version est fausse ou lourde en présence de plusieurs producteurs et/ou consommateurs.

Une solution consiste à avoir deux files d’attente (= deux variables condition) : l’une pour les consommateurs, l’autre pour les producteurs, et à réveiller sélectivement le thread adéquat.

Travail à effectuer

Créez le projet BALMultiThreadsBoost.

Pour la fin de l’exercice, vous aurez besoin de la fonction Rand().

Modifiez le fichier .pro du projet en conséquence.

Implémentez sur le principe indiqué plus haut la classe C_BALGen d’une boîte aux lettres générique (le “type” de la lettre), ayant pour interface un constructeur, un destructeur, et les deux primitives :

        void Deposer (const T & Val);
        T Retirer (void);

Testez en créant plusieurs producteurs et plusieurs consommateurs qui appellent les fonctions Deposer() et Retirer() avec des périodicités choisies ou aléatoires.

Les affichages doivent faire apparaître quel est le thread qui effectue l’action et quel est le contenu déposé ou retiré de la boîte.

Après quelques essais, vous constaterez sans doute quelques anomalies d’affichage.

Avant de lire la remarque ci-dessous, essayez de les expliquer et reportez-vous à la première partie de l’exécution commentée (plus bas).

Remarque

Les éditions ayant parfois tendance à s’inverser (malgré une exclusion mutuelle), il est intéressant d’ajouter aux opérations un estampillage : le “temps-systeme” doit être récupéré au moment où les opérations de retrait et de dépôt sont effectuées.

Pour cela, vous pouvez récupérer le temps système au moment où les opérations sont effectuées : la fonction de Boost <-- get_system_time() renvoie le temps système de type <-- system_time affichable par l’opérateur d’injection.

Il suffit d’ajouter (provisoirement) un paramètre de ce type aux fonctions Deposer() et Retirer(), et de faire afficher les temps correspondants par chaque producteur/consommateur.

Les affichages 2 et 3 présentés à la suite du paragraphe “Exécution” illustrent ce qu’on appelle la technique d’estampillage.

Exécution

La première ligne correspond à une suite de “mots” lus successivement au clavier.

Le reste du déroulement de programme peut se traduire par la trace suivante :

a z e r t y u i
Prod. 2 a déposé : z Cons. 1 a retiré : z Prod. 1 a déposé : a Cons. 2 a retiré : a
Prod. 2 a déposé : r Cons. 3 a retiré : r Prod. 1 a déposé : e
Cons. 3 a retiré : e Cons. 2 a retiré : t
Prod. 2 a déposé : t Cons. 1 a retiré : y Prod. 1 a déposé : y Cons. 2 a retiré : u
Prod. 2 a déposé : u Prod. 1 a déposé : i
Cons. 3 a retiré : i

Une première anomalie apparaît (cadre rouge) : il semble que deux consommateurs puissent prélever successivement, alors qu’aucun nouveau producteur n’ait produit !

Elle est cependant facile à expliquer : on peut vérifier que les deux consommateurs ne consomment pas la même information.

C’est donc qu’un producteur s’est intercalé entre les deux, mais ce sont les accès en exclusion mutuelle à l’écran qui a inversé les événements.

Une deuxième anomalie apparaît (cadre noir) : les lettres 'a' et 'z', semblent prélevées en ordre inverse de leur apparition dans le flux d’entrée !

Cela se reproduit aussi pour les lettres 'r' et 'e'

En fait, le 'a' a obligatoirement été lu avant le 'z' (cin est un conteneur à accès séquentiel).

Mais le premier thread producteur qui a lu 'a' n’a pas eu le temps de le déposer : le second threadproducteur a lu à son tour 'z' et l’a déposé le premier.

Le premier thread consommateur a donc lu 'z' dans la BAL, etc.

Après ajout de l’estampillage

a z e r t y u i
2008-Nov-19 12:12:51.187172 : Prod. 2 a déposé : z
2008-Nov-19 12:12:51.187547 : Cons. 1 a retiré : z
2008-Nov-19 12:12:52.187153 : Prod. 1 a déposé : a
2008-Nov-19 12:12:52.187329 : Cons. 2 a retiré : a
2008-Nov-19 12:12:55.187577 : Prod. 2 a déposé : r
2008-Nov-19 12:12:55.187752 : Cons. 3 a retiré : r
2008-Nov-19 12:12:57.187359 : Prod. 1 a déposé : e
2008-Nov-19 12:12:59.187873 : Cons. 3 a retiré : e
2008-Nov-19 12:13:03.187792 : Cons. 2 a retiré : t
2008-Nov-19 12:13:03.187782 : Prod. 2 a déposé : t
2008-Nov-19 12:13:04.187567 : Cons. 1 a retiré : y
2008-Nov-19 12:13:04.187556 : Prod. 1 a déposé : y
2008-Nov-19 12:13:08.188564 : Cons. 2 a retiré : u
2008-Nov-19 12:13:08.188553 : Prod. 2 a déposé : u
2008-Nov-19 12:13:11.187859 : Prod. 1 a déposé : i
2008-Nov-19 12:13:11.187991 : Cons. 3 a retiré : i

Remarque : en fait, c’est le premier affichage plus haut qui a été obtenu en supprimant l’estampillage !!!

En effet, rien ne prouve que les messages apparaîtraient dans le même ordre lors d’une seconde exécution !

Après remise en ordre chronologique des messages, les deux dernières anomalies disparaissent et li ne reste que l’inversion des deux premiers caractères, qui ne dépendent pas de la boîte aux lettres mais de l’accès à l’écran.

a z e r t y u i
2008-Nov-19 12:12:51.187172 : Prod. 2 a déposé : z
2008-Nov-19 12:12:51.187547 : Cons. 1 a retiré : z
2008-Nov-19 12:12:52.187153 : Prod. 1 a déposé : a
2008-Nov-19 12:12:52.187329 : Cons. 2 a retiré : a
2008-Nov-19 12:12:55.187577 : Prod. 2 a déposé : r
2008-Nov-19 12:12:55.187752 : Cons. 3 a retiré : r
2008-Nov-19 12:12:57.187359 : Prod. 1 a déposé : e
2008-Nov-19 12:12:59.187873 : Cons. 3 a retiré : e

2008-Nov-19 12:13:03.187782 : Prod. 2 a déposé : t
2008-Nov-19 12:13:03.187792 : Cons. 2 a retiré : t
2008-Nov-19 12:13:04.187556 : Prod. 1 a déposé : y
2008-Nov-19 12:13:04.187567 : Cons. 1 a retiré : y
2008-Nov-19 12:13:08.188553 : Prod. 2 a déposé : u
2008-Nov-19 12:13:08.188564 : Cons. 2 a retiré : u

2008-Nov-19 12:13:11.187859 : Prod. 1 a déposé : i
2008-Nov-19 12:13:11.187991 : Cons. 3 a retiré : i

M4104C-boost-Exo6-Corrigé

/**
*
*  @file    BALMultiThreadsBoost.cpp
*
*  @author  D. onethieu, M. Laporte
*
*  @date    19/10/2008
*
*  @version 1.0
*
*  @brief   Boite a lettres generique
*
**/
#include <iostream>
#include <cstdlib>      // srand(), rand()
#include <ctime>       // time()

#include <boost/thread/thread.hpp>
#include <boost/thread/locks.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

//#include "nsUtil.h"

using namespace std;
using namespace boost;
using namespace boost::posix_time; // time_duration
//using namespace nsUtil;

namespace
{
   typedef string CMessage;

   mutex  myMtxKeyBoard;
   mutex  myMtxScreen;

   template <class Letter>
   class LetterboxGen
   {
       condition_variable  myCondVarProd;
       condition_variable  myCondVarConsum;
       mutex               myMtxCondVar;
       Letter              myLetter;
       bool                myIsEmpty;

     public :
       LetterboxGen (void) : myIsEmpty (true) {}

       void push (const Letter & oneLetter, system_time & stamp)
       {
           {
               unique_lock <mutex> oneLock (myMtxCondVar);
               if (!myIsEmpty) myCondVarProd.wait (oneLock);

               stamp    = get_system_time();
               myLetter = oneLetter;
               myIsEmpty = false;
           }
           myCondVarConsum.notify_one();

       } // push()

       Letter pop (system_time & stamp)
       {
           Letter oneLetter;
           {
               unique_lock <mutex> oneLock (myMtxCondVar);
               if (myIsEmpty) myCondVarConsum.wait (oneLock);

               stamp    = get_system_time();
               oneLetter = myLetter;
               myIsEmpty = true;
           }
           myCondVarProd.notify_one();

           return oneLetter;

       } // pop()

   }; // LetterboxGen

   typedef LetterboxGen <CMessage> LetterboxString;

   LetterboxString theBox;

   unsigned rand (unsigned min, unsigned max)
   {
       return std::rand () % (max - min) + min;
   } // rand()

   class Producer
   {
       static unsigned s_ID;

       unsigned  myID;

     public :
       Producer (void) : myID (++s_ID) {}

       void operator() (void)
       {
           system_time stamp;
           string      oneLetter;
           for ( ; ; )
           {
               {
                   lock_guard <mutex> oneLock (myMtxKeyBoard);
                   cin >> oneLetter;
               }
               if (cin.eof ()) break;
               this_thread::sleep (seconds (rand (1, 5)));

               theBox.push (oneLetter, stamp);
               {
                   lock_guard <mutex> oneLock (myMtxScreen);
                   cout <<  stamp << " : Prod. " << myID
                        << " a déposé : " << oneLetter << '\n';
               }
               this_thread::sleep (seconds (rand (1, 5)));
           }
           {
               lock_guard <mutex> oneLock (myMtxScreen);
               cout << "Producteur " << myID << " se termine\n";
           }
       } // operator ()

   }; // Producer

   class Consumer
    {
       static unsigned s_ID;

       unsigned  myID;

     public :
       Consumer (void) : myID (++s_ID) {}

       void operator() (void)
       {
           system_time stamp;
           string      oneLetter;
           for ( ; ; )
           {
               this_thread::sleep (seconds (rand (1, 5)));
               oneLetter = theBox.pop (stamp);
               {
                   lock_guard <mutex> oneLock (myMtxScreen);
                   cout <<  stamp << " : Cons. " << myID
                        << " a retiré : " << oneLetter << '\n';
               }
               this_thread::sleep (seconds (rand (1, 5)));
           }
       } // operator ()

   }; // Consumer

   unsigned Producer  ::s_ID (0);
   unsigned Consumer::s_ID (0);


} // namespace

int main()
{
   srand (time (nullptr));
   unsigned NbConsumateurs = 3;
   unsigned NbProducteurs   = 2;

   vector <thread *> VThreads;

   for (unsigned i = NbProducteurs; i--; )
        VThreads.push_back (new thread (Producer()));

   for (unsigned i = NbConsumateurs; i--; )
        VThreads.push_back (new thread (Consumer()));

   for (unsigned i = NbProducteurs + NbConsumateurs; i--; )
        VThreads[i]->join ();

   return 0;

} // main()

M4104C-TP-Boost-Exercice7

Exercice donné en test le 06/04/2007

Remarque préliminaire :

cet exercice utilise les résultats de l’exercice BAL multi-producteurs/multi-consommateurs.

Il ne peut donc être effectué qu’après ce dernier.

Il est assez facile d’écrire un itérateur sur un conteneur “linéaire” comme un vector ou une liste chaînée par exemple.

Il suffit en effet de mémoriser dans cet itérateur un pointeur sur l’élément courant.

Le passage à l’élément suivant est obtenu en incrémentant ce pointeur d’une unité pour un vector, ou en appelant la fonction GetSuivant() de l’élément courant de la liste.

C’est beaucoup plus délicat de proposer un itérateur sur un arbre.

En effet, il ne suffit pas de garder un pointeur sur un nœud de l’arbre pour pouvoir passer au suivant : il faut aussi garder tout le chemin parcouru depuis la racine, afin de pouvoir éventuellement remonter dans l’arbre pour en explorer une nouvelle branche, ou de revenir au cœur des appels récursifs pour continuer le parcours, ce qui est totalement impossible.

Les threads offrent une alternative très élégante à ce problème.

Le principe

Un thread parcourt récursivement tous les nœuds de l’arbre (parcours infixé par exemple).

Le traitement du nœud courant consiste à déposer un pointeur sur ce nœud dans une BAL qui, par définition, est bloquante tant que l’information n’a pas été retirée par un consommateur.

Lorsque l’arbre est entièrement parcouru, le thread dépose un pointeur nul dans la BAL.

Un second thread retire successivement de la BAL chaque nœud, qu’il traite, jusqu’à trouver un pointeur de nœud invalide.

L’existant

Pour simplifier, nous travaillerons sur un arbre de recherche d’entiers, déclaré ainsi :

    class CNode;
    typedef CNode * PNode_t;

    class CNode
    {
        int     m_Val;
        PNode_t m_fg;
        PNode_t m_fd;

      public :
        PNode_t GetGauche (void)       const { return m_fg; }
        PNode_t GetDroit  (void)       const { return m_fd; }
        void    SetGauche (PNode_t fg)       { m_fg = fg;   }
        void    SetDroit  (PNode_t fd)       { m_fd = fd;   }

        CNode (int Val, PNode_t fg = 0, PNode_t fd = 0)
            : m_Val (Val), m_fg (fg), m_fd (fd) {}

        int GetVal (void) const { return m_Val; }

    }; // CNode

La classe CSearchTree encapsule la structure arborescente :

    class CSearchTree
    {
        PNode_t m_Racine;

        PNode_t AddNode (int Val, PNode_t Racine);

      public :
        CSearchTree (void) : m_Racine (0) {}
        void AddNode (int Val);

    }; // CSearchTree

La fonction publique non récursive AddNode() appelle la fonction privée récursive AddNode() qui ajoute dans l’arbre la valeur passée en paramètre.

De plus, nous disposons de la classe générique C_BALGen suivante :

    template <typename T>
    class C_BALGen
    {
        // ...
      public :
         C_BALGen (void);
        ~C_BALGen (void);

        void Deposer (const T & Val);
        T    Retirer (void);

    }; // C_BALGen

Classe CIterTree

L’interface (partie visible) de la classe CIterTree est la suivante :

    class CIterTree
    {
        // ...

      public :
        CIterTree (CSearchTree & Arbre);

        PNode_t GetNext (void);

    }; // CIterTree

Un itérateur CIterTree comporte aussi (données membres):

  • une BAL qui servira à communiquer les pointeurs des différents noeuds rencontrés,

  • une association à l’arbre qu’il doit parcourir (reçu à la construction).

L’utilisateur n’a plus qu’à appeler la fonction GetNext() pour obtenir un pointeur vers le nœud courant (avec passage au nœud suivant).

La fonction GetNext() renvoie un pointeur nul lorsque tout l’arbre a été exploré.

Voici un exemple d’utilisation par un thread utilisateur :

void ThreadUser (void)
{
    // ...
    CSearchTree Arbre;

    // Remplissage de l'arbre

    CIterTree Iter (Arbre);

    for (PNode_t Ptr; (Ptr = Iter.GetNext()); )
    {
        cout << "Valeur lue : " << Ptr->GetVal() << '\n';
        this_thread::sleep (seconds (1));
    }
    // ...

} // ThreadUser()

La classe CIterTree a deux fonctions membres privées :

void Parcours (PNode_t Racine);
void Parcours ();

La première, récursive, parcourt dans l’ordre infixé l’arbre à partir du nœud qui lui est passé en paramètre.

Rappel d’un parcours d’arbre :

void Parcours (PNode_t Node)
{
    if (!Node) return;

    // Traitement préfixé de *Node

    Parcours (Node->GetGauche());

    // Traitement infixé de *Node

    Parcours (Node->GetDroit());

    // Traitement postfixé de *Node

} // Parcours()

La seconde, non récursive, sert à amorcer la récursivité sur la totalité de l’arbre associé à l’itérateur.

Le constructeur de CIterTree lance un thread ParcoursThr() (fonction membre statique de la classe) dont le paramètre est un pointeur vers l’itérateur d’arbre qui est en train de le lancer.

Ce thread parcourt l’arbre associé à l’itérateur qui l’a lancé à travers la fonction Parcours()) qui dépose successivement dans la BAL de l’itérateur tous les nœuds rencontrés, puis un pointeur nul.

Le ThreadUser récupère les nœuds dans la BAL de l’itérateur à travers la fonction GetNext().

Travail à effectuer

Créer le projet IterArbreThreadsBoost.

Dans l’espace de noms anonyme du fichier IterArbreThreadsBoost.cpp, recopier la classe CBALGen générique ci-dessous :

    template <class CLettre>
    class CBALGen
    {
        condition_variable  m_CondVarProd;
        condition_variable  m_CondVarConsomm;
        mutex               m_MtxCondVar;
        CLettre             m_Lettre;
        bool                m_IsVide;

      public :
        CBALGen () : m_IsVide (true) {}

        void Deposer (const CLettre & MaLettre)
        {
            {
                unique_lock <mutex> Lock (m_MtxCondVar);
                if (!m_IsVide) m_CondVarProd.wait (Lock);

                m_Lettre = MaLettre;
                m_IsVide = false;
            }
            m_CondVarConsomm.notify_one();

        } // Deposer()

        CLettre Retirer ()
        {
            CLettre MaLettre;
            {
                unique_lock <mutex> Lock (m_MtxCondVar);
                if (m_IsVide) m_CondVarConsomm.wait (Lock);

                MaLettre = m_Lettre;
                m_IsVide = true;
           }
           m_CondVarProd.notify_one();

           return MaLettre;

        } // Retirer()

    }; // CBALGen

Recopier et compléter si nécessaire les classes CNode et CSearchTree décrites ci-dessus (en particulier, indiquer si nécessaire les friends à y ajouter).

Écrire la classe CIterTree (déclarations/définitions dans la déclaration de la classe).

Test de la classe CIterTree

Pour tester la classe CIterTree, nous vous proposons la séquence suivante :

  • dans l’espace de noms anonyme du fichier IterArbreThreadsBoost.cpp, déclarer un arbre de recherche d’entiers Arbre,

  • dans l’espace de noms anonyme du fichier IterArbreThreadsBoost.cpp, écrire la classe functor CLecteur ayant pour seule donnée-membre son ID (numéro d’ordre donné à la construction permettant d’identifier le thread correspondant),

  • ajouter à la classe CLecteur l’operator() qui :

    1. affiche le message :

      Debut du lecteur xxx
      

      xxx désigne l’ID du thread

    2. déclare un itérateur sur l’arbre Arbre déclaré précédemment,

    3. parcourt tout l’arbre en affichant à chaque noeud le message suivant :

      Valeur lue par lecteur xxx : yyy
      

      xxx désigne l’ID du thread et yyy la valeur associée au noeud courant.

      Le lecteur dort 1 seconde entre chaque affichage;

    4. affiche le message suivant après la fin du parcours et avant la fin du thread :

      Fin du lecteur xxx
      

    Dans la fonction main() :

      ajouter successivement 10 valeurs entières quelconques à l’Arbre précédemment déclaré,

    • créer un groupe de threads,

    • lancer plusieurs CLecteurs décalés dans le temps.

Vous constaterez (en principe !) que chaque lecteur accède indépendamment à tous les éléments de l’arbre.

Autre test possible


Un problème classique consiste à comparer les contenus de deux arbres binaires AVL n’ayant pas la même structure.

Il est alors nécessaire de les parcourir en parallèle (au moyen de deux itérateurs) et de comparer les valeurs obtenues.

S’il vous reste du temps, construisez deux arbres AVL de structures différentes mais de contenus identiques, et conparez-les.

M4104C-boost-Exo7-Corrigé

/**
*
*  @file    IterArbreThreadsBoost.cpp
*
*  @author  D. onethieu
*
*  @date    21/10/2008
*
*  @version 1.0
*
*  @brief   Iterateur a travers un arbre de recherche
*
**/
#include <iostream>

#include <boost/thread/thread.hpp>
#include <boost/thread/locks.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/condition.hpp>
#include <boost/tr1/functional.hpp>  // ou <functional> : bind()

using namespace std;
using namespace boost;
using namespace boost::posix_time;   // time_duration

namespace
{
   template <class Letter>
   class MailboxGen
   {
       condition_variable  myCondVarProd;
       condition_variable  myCondVarConsum;
       mutex               myMtxCondVar;
       Letter              myLetter;
       bool                myIsEmpty;

     public :
       MailboxGen (void) : myIsEmpty (true) {}

       void push (const Letter & oneLetter)
       {
           {
               unique_lock <mutex> oneLock (myMtxCondVar);
               if (!myIsEmpty) myCondVarProd.wait (oneLock);

               myLetter = oneLetter;
               myIsEmpty = false;
           }
           myCondVarConsum.notify_one();

       } // push()

       Letter pop (void)
       {
           Letter oneLetter;
           {
               unique_lock <mutex> oneLock (myMtxCondVar);
               if (myIsEmpty) myCondVarConsum.wait (oneLock);

               oneLetter = myLetter;
               myIsEmpty = true;
          }
          myCondVarProd.notify_one();

          return oneLetter;

       } // pop()

   }; // MailboxGen

   typedef MailboxGen <int> MailboxInt;

   class Node;
   class SearchTree;
   class IterTree;

   typedef Node * pNode_t;

   class Node
   {
       int     myValue;
       pNode_t myLeftSon;
       pNode_t myRightSon;

     public :
       pNode_t getLeftSon  (void)     const   { return myLeftSon;  }
       pNode_t getRightSon (void)     const   { return myRightSon; }
       void    setLeftSon (pNode_t LeftSon)   { myLeftSon  = LeftSon;  }
       void    setRightSon (pNode_t RightSon) { myRightSon = RightSon; }

       Node (int value, pNode_t LeftSon = 0, pNode_t RightSon = 0)
           : myValue (value), myLeftSon (LeftSon), myRightSon (RightSon) {}

       int getValue (void) const { return myValue; }

   }; // Node

   class SearchTree
   {
       friend class IterTree;

       pNode_t myRoot;

       pNode_t addNode (int value, pNode_t root)
       {
           if (! root) return new Node (value);

           if (value < root->getValue())
                  root->setLeftSon  (addNode (value, root->getLeftSon()));
           else
                  root->setRightSon (addNode (value, root->getRightSon()));

           return root;

       } // addNode()

     public :
       SearchTree (void) : myRoot (0) {}
       void addNode (int value)
       {
           myRoot = addNode (value, myRoot);

       } // addNode()

   }; // SearchTree

   class IterTree
   {
       MailboxGen <pNode_t> myMailbox;
       SearchTree *         myPTree;

       void travel (pNode_t root)
       {
           if (!root) return;

           travel (root->getLeftSon());
           myMailbox.push (root);
           travel (root->getRightSon());

       } // travel()

       void travel ()
       {
           travel (myPTree->myRoot);

           myMailbox.push (0);

       } // travel()

       static void travelThr (IterTree * pIter)
       {
           cout << "\nDebut du parcours\n";

           pIter->travel ();

           cout << "\nFin du parcours\n";

       } // travelThr()

     public :
       IterTree (SearchTree & tree)
           : myPTree (& tree)
       {
           new thread (bind (travelThr, this));

       } // IterTree()

       pNode_t getNext (void)
       {
           return myMailbox.pop();

       } // getNext()

   }; // IterTree

   SearchTree oneTree;

   class Reader
   {
       static unsigned s_ID;

       unsigned myID;

     public :
       Reader (void) : myID (++s_ID) {}

       void operator () (void)
       {
           cout << "\nDebut du lecteur " << myID << '\n';

           IterTree Iter (oneTree);

           for (pNode_t Ptr; (Ptr = Iter.getNext()); )
           {
               cout << "Valeur lue par lecteur " << myID << " : "
                    << Ptr->getValue() << '\n';
               this_thread::sleep (seconds (1));
           }
           cout << "\nFin du lecteur " << myID << '\n';

       } // operator()

   }; // Reader

   unsigned Reader::s_ID = 0;

} // namespace

int main(void)
{
   int Tab [10] = { 1, 5, 3, 6, 7, 3, 4, 11, 9, 2};

   for (unsigned i (0); i < 10; ++i) oneTree.addNode (Tab [i]);

   thread_group groupThreads;
   groupThreads.create_thread (Reader ());
   this_thread::sleep (seconds (2));
   groupThreads.create_thread (Reader ());

   groupThreads.join_all ();

   return 0;

} // main()

M4104C-TP-Boost-Exercice8

Bibliothèque C et threads

Comme nous l’avons vu dans des exercices précédents, les variables locales aux threads sont stockées dans des piles séparées.

On considère donc qu’il n’y a aucun risque de concurrence entre variables locales de threads différents.

C’est ce que nous allons voir !

Dans la première partie, nous allons montrer que certaines fonctions standard du C créent à l’insu de l’utilisateur des variables globales.

Ces fonctions peuvent être appelées “simultanément” par plusieurs threads, l’accès concurrent à cette variable globale sans aucune protection peut provoquer des erreurs.

Dans la deuxième et la troisième partie, nous allons montrer que l’implémentation de certaines parties de la bibliothèque standard du C++ dans la glibc est “plus ou moins” sécurisé dans un contexte de multi-threading.

Travail demandé

Créez le projet CCppAndThreads.

Modifier le .pro du projet pour pouvoir utiliser les boost_thread et libUtil.

Partie A

Dans cet exercice, plusieurs threads créent une copie locale d’une NTCTS commune.

Chacun décompose ensuite sa donnée locale en tokens au moyen de la fonction C standard strtok().

Cet exercice doit faire apparaître une interférence entre les traitements des NTCTSs, qui sont pourtant des variables locales aux threads.

Dans l’espace de noms anonyme du fichier CCppAndThreads_a.cpp, déclarer le mutex io_Mtx destiné à effectuer les affichages en exclusion mutuelle.

Ajouter à l’espace de noms anonyme la définition de la fonction-thread Thread() de profil :

void Thread (const string * Param);

qui :

  • crée localement une NTCTS contenant la copie du contenu de Param,

  • affiche en exclusion mutuelle la NTCTS créée.

Dans la fonction main() (le main-thread) :

  1. fabriquer une longue chaîne (string), par exemple en concaténant 200 000 fois le même mot (terminé par un espace),

  2. récupérer le nombre de threads à lancer, passé en argument de la commande,

  3. lancer les threads en leur passant en paramètre la chaîne obtenue par concaténation.

Au début de l’espace de noms anonyme, ajouter la ligne :

typedef vector <char *> CVpChar;    // vector de NTCTS

Dans la fonction Thread(), déclarer un tableau de NTCTS de type CVpChar, puis décomposer la NTCTS locale en mots grâce à la fonction strtok(), en prenant l’espace comme séparateur.

A chaque itération de cette décomposition :

  • appeler la fonction strtok()

  • ajouter l’adresse du nouveau token dans le tableau,

En fin de Thread(), afficher en exclusion mutuelle le nombre de mots trouvés (en principe 200 000 !!!)

Compilez.

Ouvrez un terminal et testez, d’abord avec un seul thread pour vérifier le bon fonctionnement de votre fonction et connaître le nombre de mots, puis avec deux ou trois threads (ça devrait suffire).

En principe, vous devriez constater quelques petits défauts … Pour quelle raison, à votre avis ?

Remarque :

Il est très surprenant que ces threads interfèrent alors qu’ils n’ont, en principe, aucune donnée commune, une fois la NTCTS recopiée localement.

Comme ce n’est pas cette copie qui provoque l’erreur, il faut aller chercher beaucoup plus profondément, dans l’implémentation même de la fonction strtok().

Lors de son premier appel, le premier paramètre lui indique l’adresse mémoire à partir de laquelle doit être effectué le découpage de la chaîne.

Dans les appels suivants, c’est un pointeur nul qui lui est passé.

Cela signifie que le découpage doit continuer à partir de la position courante dans la chaîne.

La fonction conserve donc entre chaque appel la position courante dans la chaîne traîtée, jusqu’à une prochaine réinitialisation.

On peut imaginer que la fonction a la structure suivante :

char *strtok (char *s, const char * delim)
{
    static char * Debut;
    if (s) Debut = s;
	
    char * Result = Debut;

    char * Ptr = Debut;
    // balayage de la NTCTS au moyen du pointeur Ptr jusqu'à ce qu'un
    //     délimiteur soit atteint
    *Ptr = '\0';
	
    // suite du balayage de la NTCTS au moyen du pointeur Ptr
    //     jusqu'à ce que tous les délimiteurs consécutifs aient été 
    //     sautés.
    // Ptr pointe alors sur le début du prochain token
	
    Debut = Ptr; // ou analogue !
    return Result;

} // strtok()

Cette fonction garde donc dans une variable globale, statique et unique une valeur qui mémorise l’avancement dans une NTCTS donnée.

Son utilisation dans un contexte multi-threading a donc toutes les “chances” de provoquer un accès concurrent à cette donnée involontairement partagée.

Plusieurs autres fonctions C effectuent un traitement analogue, et doivent donc être utilisées avec infiniment de précaution.

Il s’agit en particulier de :

rand()
strtok()
asctime()
ctime()
gmtime()
localtime()

Il existe des bibliothèques C écrites spécialement pour être utilisées dans un tel contexte.

Le man de la fonction strtok() indique d’ailleurs ce risque d’erreur dans un contexte de multi-threading et conseille l’utilisation de la fonction strtok_r().

Suite

Modifier l’exercice en remplaçant la fonction strtok() par strtok_r() (le suffixe _r signifie “réentrant” et est utilisé dans la bibliothèque GLibC pour toutes les fonctions dont le code est susceptible d’être parcouru par plusieurs exécutions simultanées – c’est le cas du multi-threading).

#include <string.h>

char * strtok_r (char * str, const char * delim, char **saveptr);

saveptr est un paramètre résultat de type char *, à utiliser ainsi :

char * Ptr;
... strtok_r (..., ..., & Ptr);

Constatez-vous une amélioration ?

Partie B

Classe string C++ et threads

Il y a deux façons de considérer qu’une bibliothèque est thread-safe :

  • la bibliothèque garantit que les actions en parallèle sur deux objets distincts de la même classe n’interagissent pas.

    C’est ce qu’indique par exemple la documentation de l’implémentation SGI :

    Client must lock shared mutable containers

    The SGI implementation of STL is thread-safe only in the sense that simultaneous accesses to distinct containers are safe, and simultaneous read accesses to to shared containers are safe. If multiple threads access a single container, and at least one thread may potentially write, then the user is responsible for ensuring mutual exclusion between the threads during the container accesses.

  • la bibliothèque garantit que les actions en parallèle sur le même objet sont cohérentes : les opérations d’écriture se font en exclusion mutuelle entre elles et en exclusion mutuelle avec les opérations de lecture.

Nous allons tout d’abord tenter de vérifier la première définition.

Dans le fichier CCppAndThreads_b.cpp, définir une string globale initialisée par 10000 '.'.

Dans la fonction Thread() ayant pour paramètre un int représentant un caractère :

  1. recopier la chaîne globale dans une variable locale.

    Inutile de faire cette opération en exclusion mutuelle (pourquoi ?)

  2. dans une boucle, remplacer chaque caractère de la chaîne locale par le caractère reçu en paramètre (temporiser chaque opération comme précédemment).

  3. en fin de boucle, vérifier si la chaîne locale ainsi créée est “cohérente” (c-à-d. si tous les caractères qu’elle contient sont identiques).

    Afficher (en exclusion mutuelle !) l’identifiant du thread (this_thread::get_id()) et le résultat de cette vérification : Chaine coherente ou Chaine non coherente

Compiler et, comme précédemment, tester d’abord avec un seul thread, puis avec plusieurs.

Que constatez-vous et qu’en concluez-vous?

Remarque :

Sans pouvoir l’affirmer (il faudrait aller voir le code source), il semble que la classe string de la bibliothèque C++ de la glibc 3.2 respecte la première définition du safethreading : les accès parallèles aux chaînes distinctes (qui partagent pourtant la même NTCTS au début) semblent ne pas perturber le fonctionnement normal (aucune anomalie relevée jusqu’à 100 threads, ce qui ne constitue aucunement une preuve !)

Partie C

Nous allons pour finir tenter de vérifier si la bibliothèque glibc vérifie la seconde définition de safethreading.

Recopier le fichier CCppAndThreads_b.cpp dans CCppAndThreads_c.cpp.

Remplacer la taille de la chaîne globale par 10 000 000.

Dans la fonction Thread(), remplir une chaîne locale par 10 000 000 fois le caractère passé en paramètre.

Affecter la chaîne locale à a chaîne globale.

Après la fin de tous les threads, afficher dans le main-thread si la chaîne globale est cohérente.

Compiler et, comme précédemment, tester d’abord avec un seul thread, puis avec plusieurs (avec un peu de chances, dix suffisent !).

Il arrive parfois que le programme plante par :

*** glibc detected *** double free or corruption (!prev): 0x0804d610 ***
Abort (core dumped)

ou par

Erreur de segmentation

Qu’en concluez-vous?

Remarque :

La glibc n’est pas safethreaded au second sens de ce terme.

C’est le cas de la majorité des implémentations courantes (SGI, HP, etc.) de la bibliothèque standard du C++, contrairement aux bibliothèques correspondantes de Java.

M4104C-boost-Exo8-Corrigé

/**
*
*  @file    CCppAndThreads_a.cpp
*
*  @authors D. Mathieu
*
*  @date    03/02/2010
*
*  @version V3.0
*
*  @brief   strtok() thread-safe ?
*
**/
#include <iostream>
#include <sstream>
#include <string>
#include <cstring>                    // strcpy(), strtok()
#include <vector>

#include <boost/thread/thread.hpp>
#include <boost/thread/locks.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/tr1/functional.hpp>  // ou <functional> pour bind()

using namespace std;
using namespace boost;

enum {KErrArg = 253 };

namespace
{
  typedef vector <char *> VpChar;

  const char * punct = " ";

  mutex io_Mtx;

  void fThread (const string * param)
  {
      char * oneString = new char [param->size() + 1];
      strcpy (oneString, param->c_str());

      /*  Affichage de la NTCTS locale  * /
      {
          lock_guard <mutex> oneLock (io_Mtx);
          cout << oneString << endl;
      }
      /*     */

      VpChar tab;

      /*     * /

      for (tab.push_back (strtok (oneString, punct));
           tab.back();
           tab.push_back (strtok (0, punct)));

      tab.pop_back();  // suppression du dernier pointeur (nul)

      /*     */

      /*     */

      char * buffer;
      for (tab.push_back (strtok_r (oneString, punct, &buffer));
           tab.back();
           tab.push_back (strtok_r (0, punct, &buffer)));

      tab.pop_back();  // suppression du dernier pointeur (nul)

      /*      * /

      /*     */
      {
          lock_guard <mutex> oneLock (io_Mtx);
          cout << "Nombre de mots : " << tab.size() << '\n';
      }
      /*     */

      delete [] oneString;
      /*     */

  } // fThread()

} // namespace

int main (int argc, char * argv [])
{
  if (2 != argc)
  {
      cerr << "Usage : " << argv [0] << " <Nb_threads>\n";
      return KErrArg;
  }
  unsigned nbThreads;
  {
       istringstream is (argv [1]);
       is >> nbThreads;
  }
  string line;
  for (unsigned i (200000) ; i--; ) line += "Toto ";

  thread_group groupThreads;

  for (unsigned i = nbThreads; i--; )
  {
      groupThreads.add_thread (new thread (bind <void> (fThread, & line)));
  }
  groupThreads.join_all ();

  return 0;

} // main()