IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Threads & Processus C++ Discussion :

Objets et méthodes volatiles : proposition de Andrei Alexandrescu


Sujet :

Threads & Processus C++

  1. #1
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut Objets et méthodes volatiles : proposition de Andrei Alexandrescu
    Le 1er février 2001, Andrei Alexandrescu a publié un article intitulé "volatile: The Multithreaded Programmer's Best Friend".
    Lien : http://www.drdobbs.com/cpp/volatile-...rs-b/184403766
    Lien web.archive.org : https://web.archive.org/web/20160327...rs-b/184403766

    (Remarque : Quand je donne un lien du site www.drdobbs.com, au cas où ce dernier vous casse les pieds en vous redirigeant vers une page avec un message « Looks like you've hit your 2 article limit. Log-in or register for a free account to get unlimited articles and full access to Dr.Dobbs. », je vous donne aussi un lien équivalent via web.archive.org qui permet de contourner ce problème. Une autre solution est de supprimer en local les cookies du site www.drdobbs.com.)

    Le but est de contrôler à la compilation que les ressources accessibles par plusieurs threads sont biens protégées avant d'être utilisées.

    Je vous résume les idées essentielles, mais ce sera plus facile à suivre en lisant directement l'article :
    • Quand une variable est en dehors d'une section critique, elle est volatile. Quand une variable est dans une section critique, elle est non volatile.
    • On a un modèle de classe LockingPtr dont le constructeur bloque un mutex, dont le destructeur débloque ce mutex et qui convertit une référence vers une variable volatile en une référence vers type non volatile avec un const_cast. Voici le code donné par Andrei Alexandrescu :
      Code : Sélectionner tout - Visualiser dans une fenêtre à part
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      template <typename T>
      class LockingPtr {
      public:
         // Constructors/destructors
         LockingPtr(volatile T& obj, Mutex& mtx)
             : pObj_(const_cast<T*>(&obj)),
              pMtx_(&mtx)
         {    mtx.Lock();    }
         ~LockingPtr()
         {    pMtx_->Unlock();    }
         // Pointer behavior
         T& operator*()
         {    return *pObj_;    }
         T* operator->()
         {   return pObj_;   }
      private:
         T* pObj_;
         Mutex* pMtx_;
         LockingPtr(const LockingPtr&);
         LockingPtr& operator=(const LockingPtr&);
      };
    • Voici des exemples donnés par Andrei Alexandrescu :
      Code : Sélectionner tout - Visualiser dans une fenêtre à part
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class SyncBuf {
      public:
          void Thread1();
          void Thread2();
      private:
          typedef vector<char> BufT;
          volatile BufT buffer_;
          Mutex mtx_; // controls access to buffer_
      };
       
      void SyncBuf::Thread1() {
          LockingPtr<BufT> lpBuf(buffer_, mtx_);
          BufT::iterator i = lpBuf->begin();
          for (; i != lpBuf->end(); ++i) {
              ... use *i ...
          }
      }
      Code : Sélectionner tout - Visualiser dans une fenêtre à part
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class Widget
      {
      public:
          void Operation() volatile;
          void Operation();
          ...
      private:
          Mutex mtx_;
      };
       
      void Widget::Operation() volatile
      {
          LockingPtr<Widget> lpThis(*this, mtx_);
          lpThis->Operation(); // invokes the non-volatile function
      }

    Pour rappel, le compilateur fait des contrôles sur le mot-clé volatile qui sont similaires à ceux de const : On peut convertir implicitement de Type& vers volatile Type&. Par contre, une tentative de conversion sans const_cast de volatile Type& vers Type& entraîne une erreur de compilation. Cette caractéristique est utilisée ici de telle sorte que, si on utilise directement un objet volatile avec des méthodes non volatiles sans protéger cet objet par un mutex, on a une erreur de compilation.

    L'exemple de Andrei Alexandrescu avec le vector<char> a un gros problème : il va à l'encontre du standard C++.
    En effet, il accède à une variable définie comme volatile (SyncBuf::buffer_) via une lvalue qui n'est pas volatile (*lpThis.pObj_).
    C'est illégal.
    « If an attempt is made to refer to an object defined with a volatile-qualified type through the use of a glvalue with a non-volatile-qualified type, the program behavior is undefined. » (C++11 7.1.6.1/6)

    Autre source : https://www.securecoding.cert.org/co...qualified+type
    Rappel : Une lvalue est un cas particulier de glvalue.

    Pour contourner ce genre de problème, il faut définir une variable non volatile et la manipuler à travers une référence vers type volatile :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    Type variableCachee(blabla);
    volatile Type& variable = variableCachee;
    variableCachee n'est pas volatile, donc on a le droit de faire const_cast<Type&>(variable).

    A part ça, l'exemple de Andrei Alexandrescu avec la classe Widget ne compile que si sa classe Mutex a des fonctions membres Lock et Unlock volatiles.

    De plus, Il faudrait changer un peu LockingPtr pour prendre en compte les mutex de la STL en C++11.

    Alors, on pourrait faire du code qui ressemble à ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    // Dans LockingPtr.h :
    template<typename T, class Mutex>
    class LockingPtr
    {
        LockingPtr(const LockingPtr&)            = delete;
        LockingPtr(LockingPtr&&)                 = delete;
        LockingPtr& operator=(const LockingPtr&) = delete;
        LockingPtr& operator=(LockingPtr&&)      = delete;
    private:
        T&     m_obj;
        Mutex& m_mtx;
    public:
        LockingPtr(volatile T& obj, volatile Mutex& mtx) :
            m_obj(const_cast<T&>    (obj)),
            m_mtx(const_cast<Mutex&>(mtx))
                        {m_mtx.lock();    }
        ~LockingPtr()   {m_mtx.unlock();}
        T& operator*()  {return  m_obj; }
        T* operator->() {return &m_obj; }
    };
     
    // Soit deux classes Foo et Bar. Pour éviter les deadlocks,
    // posons comme convention que le mutex de Foo doit être bloqué avant celui de Bar.
     
    // Dans Foo.h :
    class Foo
    {
    private:
        std::mutex m_mtx;
        bool       m_work;
        // ...
    public:
        void doA(volatile Bar& bar) volatile;
        void doA(volatile Bar& bar);
        void doC(Bar& bar);
        // pas de fonction volatile avec un paramètre Bar non volatile
        // car le mutex de Foo doit être bloqué avant celui de Bar.
        // ...
    };
     
    // Dans Bar.h :
    class Bar
    {
    private:
        std::mutex m_mtx;
        // ...
    public:
        void doB(Foo& foo) volatile;
        // pas de fonction non volatile avec un paramètre Foo volatile
        // car le mutex de Foo doit être bloqué avant celui de Bar.
        // ...
    };
     
    // Dans Foo.cpp :
    void Foo::doA(volatile Bar& bar) volatile
    {
        LockingPtr<Foo, std::mutex> lockedThis(*this, m_mtx);
        lockedThis->doA(bar);
    }
    void Foo::doA(volatile Bar& bar)
    {
        if(m_work) {
            bar.doB(*this);
        }
    }
    void Foo::doC(Bar& bar)
    {
        // *this et bar sont non volatiles, donc protégés.
        // On peut appeler toutes les fonctions que l'on veut où
        // *this et bar ne sont pas volatiles !
    }
     
    // Dans Bar.cpp :
    void Bar::doB(Foo& foo) volatile
    {
        LockingPtr<Bar, std::mutex> lockedThis(*this, m_mtx);
        Bar& castedThisRef = *lockedThis;
        foo.doC(castedThisRef);
    }
    Remarque 1 :
    Andrei Alexandrescu parle de cet article au début de son article "Generic: Min and Max Redivivus".
    Lien : http://www.drdobbs.com/generic-min-a...ivus/184403774
    Lien web.archive.org : https://web.archive.org/web/20160305...ivus/184403774

    Remarque 2 :
    Par contre, ce n'est pas cette technique qui aidera efficacement à éviter les deadlocks.
    Pour cela, il faudra la combiner avec une autre technique, par exemple les hiérarchies de locks.
    Plus de détails dans l'article "Use Lock Hierarchies to Avoid Deadlock" de Herb Sutter :
    Lien : http://www.drdobbs.com/parallel/use-...lock/204801163
    Lien web.archive.org : https://web.archive.org/web/20151123...ock/204801163?

    Qu'en pensez-vous ?

    EDIT 1 : Correction d'un lapsus dans le titre.
    EDIT 2 : Ajout de quelques clarifications pour que ce soit plus facile à suivre.
    EDIT 3 : Petit changement dans le titre.

  2. #2
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Points : 16 213
    Points
    16 213
    Par défaut
    Citation Envoyé par Pyramidev Voir le message
    Qu'en pensez-vous ?
    Que c'est une abomination...
    En gros, il utilise volatile non pas pour le fait que la variable soit volatile (ce qu'elle n'a aucune raison d'être), mais uniquement pour bénéficier de certaines validations effectuées par le système de types sur des variables volatiles. Ce faisant, il introduit de la confusion supplémentaire sur le vrai rôle de volatile (qui n'en demandait pas tant...) en plus d'empêcher le compilateur d'optimiser correctement le code.
    Je reconnais le problème initial, n'ai pas de solution alternative à proposer, mais je trouve que son remède est pire que la maladie.
    Ma session aux Microsoft TechDays 2013 : Développer en natif avec C++11.
    Celle des Microsoft TechDays 2014 : Bonnes pratiques pour apprivoiser le C++11 avec Visual C++
    Et celle des Microsoft TechDays 2015 : Visual C++ 2015 : voyage à la découverte d'un nouveau monde
    Je donne des formations au C++ en entreprise, n'hésitez pas à me contacter.

  3. #3
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut
    Après avoir bossé davantage le sujet, je crois que j'ai trouvé une alternative satisfaisante.
    J'ai bidouillé un modèle de classe Protected<T, Mutex> et j'ai remplacé les occurrences de volatile Type& par Protected<Type, std::mutex>, à quelques détails près.

    En plus, il y a un autre inconvénient en moins :
    Type& est convertible en volatile Type&, ce qui, dans la solution de Andrei Alexandrescu, risquait de favoriser des self-deadlocks.

    J'ai repris l'exemple avec mes classes Foo et Bar et je l'ai adapté à Protected<T, Mutex> :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    // Dans LockingPtr.h :
    template<typename T, class Mutex>
    class LockingPtr
    {
        LockingPtr(const LockingPtr&)            = delete;
        LockingPtr& operator=(const LockingPtr&) = delete;
        LockingPtr& operator=(LockingPtr&&)      = delete;
    private:
        T*     m_obj;
        Mutex* m_mtx;
    public:
        LockingPtr(T& obj, Mutex& mtx) :
            m_obj(&obj),
            m_mtx(&mtx)
        {
            m_mtx->lock();
        }
        LockingPtr(LockingPtr&& other) :
            m_obj(other.m_obj),
            m_mtx(other.m_mtx)
        {
            other.m_obj = nullptr;
            other.m_mtx = nullptr;
        }
        ~LockingPtr()   {if(m_mtx) m_mtx->unlock();}
        T& operator*()  {return *m_obj;            }
        T* operator->() {return  m_obj;            }
        T* get()        {return  m_obj;            }
    };
     
    // Dans Protected.h :
    template<typename T, class Mutex>
    class Protected
    {
        Protected& operator=(const Protected&) = delete;
        Protected& operator=(Protected&&)      = delete;
    private:
        T&     m_obj;
        Mutex& m_mtx;
    public:
        Protected(T& obj, Mutex& mtx) : m_obj(obj), m_mtx(mtx) {}
        Protected(const Protected&) = default;
        Protected(Protected&&)      = default;
     
        LockingPtr<T, Mutex> scopeAccess()
        {
            return LockingPtr<T, Mutex>(m_obj, m_mtx);
        }
    };
     
    // Soit deux classes Foo et Bar. Pour éviter les deadlocks,
    // posons comme convention que le mutex de Foo doit être bloqué avant celui de Bar.
     
    // Dans Foo.h :
    class Foo
    {
    private:
        std::mutex m_mtx;
        bool       m_work;
        // ...
    public:
        static void doA(Protected<Foo, std::mutex> foo,
                        Protected<Bar, std::mutex> bar);
        void doA(Protected<Bar, std::mutex> bar);
        void doC(Bar& bar);
        // pas de fonction avec Foo protégé et Bar non protégé,
        // car le mutex de Foo doit être bloqué avant celui de Bar.
        // ...
    };
     
    // Dans Bar.h :
    class Bar
    {
    private:
        std::mutex m_mtx;
        // ...
    public:
        static void doB(Protected<Bar, std::mutex> bar,
                        Foo& foo);
        // pas de fonction avec Foo protégé et Bar non protégé,
        // car le mutex de Foo doit être bloqué avant celui de Bar.
        // ...
    };
     
    // Dans Foo.cpp :
    void Foo::doA(Protected<Foo, std::mutex> foo,
                  Protected<Bar, std::mutex> bar)
    {
        auto lockedFoo = foo.scopeAccess();
        lockedFoo->doA(bar);
    }
    void Foo::doA(Protected<Bar, std::mutex> bar)
    {
        if(m_work) {
            Bar::doB(bar, *this);
        }
    }
    void Foo::doC(Bar& bar)
    {
        // code
    }
     
    // Dans Bar.cpp :
    void Bar::doB(Protected<Bar, std::mutex> bar,
                  Foo& foo)
    {
        auto lockedBar = bar.scopeAccess();
        Bar& castedBarRef = *lockedBar;
        foo.doC(castedBarRef);
    }
    Je l'ai fait un peu vite, donc il y a probablement des améliorations à apporter.
    Des suggestions ?

  4. #4
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 49
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Points : 16 213
    Points
    16 213
    Par défaut
    Je n'ai pas lu les détails de ton implémentation, mais j'ai peur que ta solution soit assez lourde à l'utilisation. C'est peut-être un cas où la surcharge de l'opérateur . pourrait simplifier les choses, et éviter de donner une sémantique de pointeur à un objet qui n'a pas de raison d'en avoir autrement.

    Sinon, je ne suis pas certain de la référence dans Protected. J'aurais bien vu cette classe comme possédant la donnée qu'elle Protège, sinon, ça veut dire qu'elle est accessible depuis un autre endroit sans mécanisme de protection. Et en teme de nommage, j'aurais plus vu un nom comme ThreadShared que Protected.

    Après, comme ça fait longtemps que je n'ai pas développé en multithread, je ne sais pas trop avec quelle fj'utiliserais une telle classe...
    Ma session aux Microsoft TechDays 2013 : Développer en natif avec C++11.
    Celle des Microsoft TechDays 2014 : Bonnes pratiques pour apprivoiser le C++11 avec Visual C++
    Et celle des Microsoft TechDays 2015 : Visual C++ 2015 : voyage à la découverte d'un nouveau monde
    Je donne des formations au C++ en entreprise, n'hésitez pas à me contacter.

  5. #5
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut
    Citation Envoyé par JolyLoic Voir le message
    Sinon, je ne suis pas certain de la référence dans Protected. J'aurais bien vu cette classe comme possédant la donnée qu'elle Protège, sinon, ça veut dire qu'elle est accessible depuis un autre endroit sans mécanisme de protection. Et en teme de nommage, j'aurais plus vu un nom comme ThreadShared que Protected.
    Remarques prises en compte.

    Grosse amélioration du code :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    // Dans MovableLockGuard.h :
    template<class Mutex>
    class MovableLockGuard
    {
        MovableLockGuard(const MovableLockGuard&)            = delete;
        MovableLockGuard& operator=(const MovableLockGuard&) = delete;
    private:
        Mutex* m_mtx;
    public:
        MovableLockGuard(Mutex& mtx)               : m_mtx(&mtx)        {m_mtx->lock();}
        MovableLockGuard(MovableLockGuard&& other) : m_mtx(other.m_mtx) {other.m_mtx = nullptr;}
        ~MovableLockGuard()
        {
            if(m_mtx)
                m_mtx->unlock();
        }
        MovableLockGuard& operator=(MovableLockGuard&& other)
        {
            if(m_mtx)
                m_mtx->unlock();
            m_mtx = other.m_mtx;
            other.m_mtx = nullptr;
        }
    };
     
    // Dans ThreadShared .h :
    template<typename T, class Mutex>
    class ThreadShared 
    {
        ThreadShared(const ThreadShared&)            = delete;
        ThreadShared(ThreadShared&&)                 = delete;
        ThreadShared& operator=(const ThreadShared&) = delete;
        ThreadShared& operator=(ThreadShared&&)      = delete;
    private:
        T      m_obj;
        Mutex& m_mtx;
    public:
        template<typename... Args>
        ThreadShared(Mutex& mtx, Args&&... args) :
            m_obj(std::forward<Args>(args)...),
            m_mtx(mtx)
        {}
     
        std::pair<const T&, MovableLockGuard<Mutex>> scopeAccess() const
        {
            std::pair<const T&, MovableLockGuard<Mutex>> result(m_obj, MovableLockGuard<Mutex>(m_mtx));
            return result;
        }
     
        std::pair<T&, MovableLockGuard<Mutex>> scopeAccess()
        {
            std::pair<T&, MovableLockGuard<Mutex>> result(m_obj, MovableLockGuard<Mutex>(m_mtx));
            return result;
        }
    };
     
    // Convention : Tout objet dont l'accès est protégé par un mutex doit être encapsulté dans un objet ThreadShared.
     
    // Soit deux classes Foo et Bar. Pour éviter les deadlocks,
    // posons comme convention que le mutex de Foo doit être bloqué avant celui de Bar.
    // Alors, il n'y a pas de fonction avec à la fois un paramètre ThreadShared<Foo, std::mutex>& et un paramètre Bar&.
     
    // Dans Foo.h :
    class Foo
    {
    private:
        bool       m_work;
        // ...
    public:
        static void doA(const ThreadShared<Foo, std::mutex>& foo,
                              ThreadShared<Bar, std::mutex>& bar);
        void doA(ThreadShared<Bar, std::mutex>& bar) const;
        void doC(Bar& bar) const;
        // ...
    };
     
    // Dans Bar.h :
    class Bar
    {
    private:
        // ...
    public:
        static void doB(ThreadShared<Bar, std::mutex>& bar,
                        const Foo& foo);
        // ...
    };
     
    // Dans Foo.cpp :
    void Foo::doA(const ThreadShared<Foo, std::mutex>& foo,
                        ThreadShared<Bar, std::mutex>& bar)
    {
        std::pair<const Foo&, MovableLockGuard<std::mutex>> access = foo.scopeAccess();
        const Foo& fooRef = access.first;
        fooRef.doA(bar);
    }
    void Foo::doA(ThreadShared<Bar, std::mutex>& bar) const
    {
        if(m_work) {
            Bar::doB(bar, *this);
        }
    }
    void Foo::doC(Bar& bar) const
    {
        // *this et bar sont protégés par un mutex bloqué chacun.
        // On peut faire ce que l'on veut avec.
    }
     
    // Dans Bar.cpp :
    void Bar::doB(ThreadShared<Bar, std::mutex>& bar,
                  const Foo& foo)
    {
        std::pair<Bar&, MovableLockGuard<std::mutex>> access = bar.scopeAccess();
        Bar& barRef = access.first;
        foo.doC(barRef);
    }
    En bref :
    • J'ai remplacé LockingPtr<T, Mutex> par std::pair<T&, MovableLockGuard<Mutex>>. Le type est plus verbeux, mais son rôle est plus clair.
    • J'ai remplacé Protected<T, Mutex> par ThreadShared<T, Mutex> qui possède un objet de type T et une référence vers un mutex.


    Cette fois, je pense que c'est un outil exploitable pour la programmation multithread.

  6. #6
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut
    Je publie une nouvelle version de mon modèle de classe ThreadShared.
    Pour rappel, le but de ce modèle est d'aider à n'utiliser un objet que quand son mutex associé est bloqué.

    Liste des principales améliorations depuis mon précédent message :
    • Possibilité de mettre en paramètre un mutex qui n'a pas la même syntaxe que les mutex de la STL.
    • Ajout de la fonctionnalité "try_lock" (si le mutex le permet) pour laisser à l'appelant le choix de faire autre chose en attendant que le mutex soit débloqué.
    • Ajout de la fonctionnalité "shared" (si le mutex le permet) pour éviter que les lecteurs ne se bloquent entre eux.


    En préliminaire, voici StdMutexAdapter, un modèle de classe pour manipuler un mutex avec la même syntaxe que celle des mutex de la STL. A titre illustratif, je publie aussi ses spécialisations pour deux classes de Qt et deux classes de la VCL (de Embarcadero).

    StdMutexAdapter.h :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    #ifndef INCLUDE__PYRAMIDEV__STD_MUTEX_ADAPTER__H
    #define INCLUDE__PYRAMIDEV__STD_MUTEX_ADAPTER__H
     
    #include <chrono>
     
    namespace pyramidev {
     
    /*!
     * \brief   Adaptateur pour manipuler un mutex avec la syntaxe des mutex de la STL.
     * \details Il faut spécialiser ce modèle pour les mutex qui n'ont pas la même syntaxe que
     *          ceux de la STL.
     * \remark  Un intérêt de cet adaptateur est de pouvoir utiliser pyramidev::ThreadShared.\n
     *          Un autre intérêt est de pouvoir profiter des templates de la STL pour manipuler les mutex
     *          (std::lock, std::lock_guard, std::unique_lock, std::shared_lock, etc).
     */
    template<class Mutex>
    class StdMutexAdapter
    {
    private:
    	Mutex& m_mtx;
     
    public:
    	typedef Mutex mutex_type;
     
    	// Conversion implicite
    	StdMutexAdapter(Mutex& mtx) : m_mtx(mtx) {}
     
    	void lock()        { m_mtx.lock();        }
    	void lock_shared() { m_mtx.lock_shared(); }
     
    	void unlock()        { m_mtx.unlock();        }
    	void unlock_shared() { m_mtx.unlock_shared(); }
     
    	bool try_lock()        { return m_mtx.try_lock();        }
    	bool try_lock_shared() { return m_mtx.try_lock_shared(); }
     
    	template<class Rep, class Period>
    	bool try_lock_for(const std::chrono::duration<Rep,Period>& timeout_duration)
    	{
    		return m_mtx.try_lock_for(timeout_duration);
    	}
     
    	template<class Rep, class Period>
    	bool try_lock_shared_for(const std::chrono::duration<Rep,Period>& timeout_duration)
    	{
    		return m_mtx.try_lock_shared_for(timeout_duration);
    	}
     
    	template<class Clock, class Duration>
    	bool try_lock_until(const std::chrono::time_point<Clock,Duration>& timeout_time)
    	{
    		return m_mtx.try_lock_for(timeout_time);
    	}
     
    	template<class Clock, class Duration>
    	bool try_lock_shared_until(const std::chrono::time_point<Clock,Duration>& timeout_time)
    	{
    		return m_mtx.try_lock_shared_for(timeout_time);
    	}
    };
     
    } // namespace pyramidev
     
    #endif
    StdMutexAdapterQt.h :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    #ifndef INCLUDE__PYRAMIDEV__STD_MUTEX_ADAPTER_QT__H
    #define INCLUDE__PYRAMIDEV__STD_MUTEX_ADAPTER_QT__H
     
    /*!
     * \file  StdMutexAdapterQt.h
     * \brief Ce fichier définit les spécialisations de pyramidev::StdMutexAdapter pour les mutex de Qt.
     */
     
    #include "StdMutexAdapter.h"
     
    #include <QMutex>
    #include <QReadWriteLock>
     
    namespace pyramidev {
     
    /*!
     * \brief  Adaptateur pour manipuler un QMutex de Qt avec la syntaxe d'un std::timed_mutex (C++11).
     * \remark Documentation de QMutex : http://doc.qt.io/qt-5/qmutex.html
     * \remark Documentation de std::timed_mutex : http://en.cppreference.com/w/cpp/thread/timed_mutex
     */
    template<>
    class StdMutexAdapter<QMutex>
    {
    private:
    	QMutex& m_mtx;
     
    public:
    	typedef QMutex mutex_type;
     
    	// Conversion implicite
    	StdMutexAdapter(QMutex& mtx) : m_mtx(mtx) {}
     
    	void lock()     { m_mtx.lock();           }
    	void unlock()   { m_mtx.unlock();         }
    	bool try_lock() { return m_mtx.tryLock(); }
     
    	bool try_lock_for(const std::chrono::milliseconds& timeout_duration)
    	{
    		return m_mtx.tryLock(timeout_duration.count());
    	}
    };
     
    /*!
     * \brief  Adaptateur pour manipuler un QReadWriteLock de Qt avec la syntaxe d'un std::shared_timed_mutex (C++17).
     * \remark Documentation de QReadWriteLock : http://doc.qt.io/qt-5/qreadwritelock.html
     * \remark Documentation de std::shared_timed_mutex : http://en.cppreference.com/w/cpp/thread/shared_timed_mutex
     */
    template<>
    class StdMutexAdapter<QReadWriteLock>
    {
    private:
    	QReadWriteLock& m_mtx;
     
    public:
    	typedef QReadWriteLock mutex_type;
     
    	// Conversion implicite
    	StdMutexAdapter(QReadWriteLock& mtx) : m_mtx(mtx) {}
     
    	void lock()        { m_mtx.lockForWrite(); }
    	void lock_shared() { m_mtx.lockForRead();  }
     
    	void unlock()        { m_mtx.unlock(); }
    	void unlock_shared() { m_mtx.unlock(); }
     
    	bool try_lock()        { return m_mtx.tryLockForWrite(); }
    	bool try_lock_shared() { return m_mtx.tryLockForRead();  }
     
    	bool try_lock_for(const std::chrono::milliseconds& timeout_duration)
    	{
    		return m_mtx.tryLockForWrite(timeout_duration.count());
    	}
     
    	bool try_lock_shared_for(const std::chrono::milliseconds& timeout_duration)
    	{
    		return m_mtx.tryLockForRead(timeout_duration.count());
    	}
    };
     
    } // namespace pyramidev
     
    #endif
    StdMutexAdapterVCL.h :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    #ifndef INCLUDE__PYRAMIDEV__STD_MUTEX_ADAPTER_VCL__H
    #define INCLUDE__PYRAMIDEV__STD_MUTEX_ADAPTER_VCL__H
     
    /*!
     * \file  StdMutexAdapter.h
     * \brief Ce fichier définit les spécialisations de pyramidev::StdMutexAdapter pour les mutex de la VCL.
     */
     
    #include "StdMutexAdapter.h"
     
    #ifndef INC_VCL
    #include <vcl.h>
    #endif
     
    namespace pyramidev {
     
    /*!
     * \brief  Adaptateur pour manipuler un TCriticalSection de la VCL avec la syntaxe d'un std::mutex (C++11).
     * \remark Documentation de TCriticalSection : http://docwiki.embarcadero.com/Libraries/Seattle/fr/System.SyncObjs.TCriticalSection
     * \remark Documentation de std::mutex : http://en.cppreference.com/w/cpp/thread/mutex
     */
    template<>
    class StdMutexAdapter<TCriticalSection>
    {
    private:
    	TCriticalSection& m_mtx;
     
    public:
    	typedef TCriticalSection mutex_type;
     
    	// Conversion implicite
    	StdMutexAdapter(TCriticalSection& mtx) : m_mtx(mtx) {}
     
    	void lock()   { m_mtx.Acquire(); }
    	void unlock() { m_mtx.Release(); }
    };
     
    /*!
     * \brief  Adaptateur pour manipuler un TMultiReadExclusiveWriteSynchronizer de la VCL avec la syntaxe d'un std::shared_mutex (C++17).
     * \remark Documentation de TMultiReadExclusiveWriteSynchronizer : http://docwiki.embarcadero.com/Libraries/Seattle/fr/System.SysUtils.TMultiReadExclusiveWriteSynchronizer
     * \remark Documentation de std::shared_mutex : http://en.cppreference.com/w/cpp/thread/shared_mutex
     */
    template<>
    class StdMutexAdapter<TMultiReadExclusiveWriteSynchronizer>
    {
    private:
    	TMultiReadExclusiveWriteSynchronizer& m_mtx;
     
    public:
    	typedef TMultiReadExclusiveWriteSynchronizer mutex_type;
     
    	// Conversion implicite
    	StdMutexAdapter(TMultiReadExclusiveWriteSynchronizer& mtx) : m_mtx(mtx) {}
     
    	void lock()          { m_mtx.BeginWrite(); }
    	void lock_shared()   { m_mtx.BeginRead();  }
    	void unlock()        { m_mtx.EndWrite();   }
    	void unlock_shared() { m_mtx.EndRead();    }
    };
     
    } // namespace pyramidev
     
    #endif
    Ensuite, voici les modèles de classe DestructorWillUnlockStdMutex, DestructorWillUnlockSharedStdMutex, DestructorWillUnlock et DestructorWillUnlockShared.

    DestructorWillUnlock.h :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    #ifndef INCLUDE__PYRAMIDEV__DESTRUCTOR_UNLOCKS__H
    #define INCLUDE__PYRAMIDEV__DESTRUCTOR_UNLOCKS__H
     
    #include "StdMutexAdapter.h"
     
    namespace pyramidev {
     
    namespace destructor_will_x_impl {
     
    /*!
     * \brief  Modèle de classe déplaçable non copiable dont le destructeur appelle OpOnMutex sur le mutex.
     * \remark Le déplacement transfert la possession du mutex.\n
     *         Le destructeur n'appelle OpOnMutex que si l'objet possède le mutex.
     */
    template<class Mutex, class OpOnMutex>
    class DestructorWillDo
    {
    	DestructorWillDo(           const DestructorWillDo<Mutex, OpOnMutex>& other) = delete;
    	DestructorWillDo& operator=(const DestructorWillDo<Mutex, OpOnMutex>& other) = delete;
     
    private:
    	Mutex*    m_mtx;
    	OpOnMutex m_op;
     
    public:
    	DestructorWillDo()                                : m_mtx(nullptr), m_op()              {}
    	explicit DestructorWillDo(const OpOnMutex& op)    : m_mtx(nullptr), m_op(op)            {}
    	explicit DestructorWillDo(OpOnMutex&& op)         : m_mtx(nullptr), m_op(std::move(op)) {}
    	explicit DestructorWillDo(Mutex* mtx)             : m_mtx(mtx),     m_op()              {}
    	DestructorWillDo(Mutex* mtx, const OpOnMutex& op) : m_mtx(mtx),     m_op(op)            {}
    	DestructorWillDo(Mutex* mtx, OpOnMutex&& op)      : m_mtx(mtx),     m_op(std::move(op)) {}
     
    	DestructorWillDo(DestructorWillDo<Mutex, OpOnMutex>&& other) :
    		m_mtx(other.m_mtx), m_op(std::move(other.m_op))
    	{
    		other.m_mtx = nullptr;
    	}
     
    	DestructorWillDo& operator=(DestructorWillDo<Mutex, OpOnMutex>&& other)
    	{
    		m_mtx       = other.m_mtx;
    		other.m_mtx = nullptr;
    		m_op        = std::move(other.m_op);
    		return *this;
    	}
     
    	~DestructorWillDo()
    	{
    		if(m_mtx != nullptr)
    			m_op(*m_mtx);
    	}
    };
     
    template<class StdMutex>
    struct Unlock {
    	void operator()(StdMutex& mut) const { mut.unlock(); }
    };
     
    template<class StdMutex>
    struct UnlockShared {
    	void operator()(StdMutex& mut) const { mut.unlock_shared(); }
    };
     
    } // namespace destructor_will_x_impl
     
    /*!
     * \brief  Modèle de classe déplaçable non copiable dont le destructeur appelle unlock sur le mutex.
     * \remark Le déplacement transfert la possession du mutex.\n
     *         Le destructeur n'appelle unlock que si l'objet possède le mutex.
     */
    template<class StdMutex>
    using DestructorWillUnlockStdMutex = destructor_will_x_impl::DestructorWillDo<
    	StdMutex, destructor_will_x_impl::Unlock<StdMutex>>;
     
    /*!
     * \brief  Modèle de classe déplaçable non copiable dont le destructeur appelle unlock_shared sur le mutex.
     * \remark Le déplacement transfert la possession du mutex.\n
     *         Le destructeur n'appelle unlock_shared que si l'objet possède le mutex.
     */
    template<class StdMutex>
    using DestructorWillUnlockSharedStdMutex = destructor_will_x_impl::DestructorWillDo<
    	StdMutex, destructor_will_x_impl::Unlock<StdMutex>>;
     
     
    template<class Mutex>
    using DestructorWillUnlock = DestructorWillUnlockStdMutex<StdMutexAdapter<Mutex>>;
     
    template<class Mutex>
    using DestructorWillUnlockShared = DestructorWillUnlockSharedStdMutex<StdMutexAdapter<Mutex>>;
     
    } // namespace pyramidev
     
    #endif
    Voici enfin le modèle de classe ThreadShared.

    ThreadShared.h :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    #ifndef INCLUDE__PYRAMIDEV__THREAD_SHARED__H
    #define INCLUDE__PYRAMIDEV__THREAD_SHARED__H
     
    /*!
     * \file    ThreadShared.h
     * \brief   Définit le modèle de classe ThreadShared qui aide à n'utiliser un objet que quand son mutex associé est bloqué.
     * \details Contexte :\n
     *          Quand on a une référence vers un objet, comment savoir si l'accès à cet objet est déjà protégé ?\n
     *          Une première solution est de documenter ou de choisir les bons noms de variable :\n
                \code
                void toto(Bar& bar_a_proteger, const Foo& foo_deja_protege)
                {
                    // Chercher le mutex associé à bar_a_proteger et le bloquer.
                    // code...
                }
                \endcode
     *          Le problème, c'est que cela demande de la rigueur et que ce n'est pas contrôlé à la compilation.\n
     *          Une autre solution consiste à mettre systématiquement ensemble l'objet et la référence vers
     *          le mutex pour signaler qu'il faut protéger l'objet :\n
                \code
                void toto(std::pair<Bar, std::mutex&> bar, const Foo& foo)
                {
                    std::lock_guard<std::mutex> lock(bar.second);
     
                    // foo n'est pas accompagné d'un mutex ici, donc on considère qu'il est déjà protégé,
                    // ou bien qu'il n'est pas partagé par plusieurs processus.
     
                    // code...
                }
                \endcode
     *          La solution avec ThreadShared est similaire sauf que, à la place de
     *          std::pair<Bar, std::mutex&>, on utilise ThreadShared<Bar, std::mutex>
     *          dont l'interface oblige à bloquer le mutex pour accéder à l'objet et force le déblocage
     *          du mutex quand on sort du bloc où on a bloqué le mutex
     *          (sauf bidouille intentionnelle de l'utilisateur avec la sémantique de mouvement).
                \code
                void toto(pyramidev::ThreadShared<Bar, std::mutex>& bar, const Foo& foo)
                {
                    std::pair<Bar&, pyramidev::DestructorWillUnlock<std::mutex>> access = bar.scopeAccess();
                    Bar& barRef = access.first; // en attendant les structured bindings de C++17
     
                    // foo n'est pas dans un ThreadShared, donc on considère qu'il est déjà protégé,
                    // ou bien qu'il n'est pas partagé par plusieurs processus.
     
                    // code
                }
     
                void titi(const pyramidev::ThreadShared<Foo, std::shared_mutex>& foo)
                {
                    std::pair<const Foo&, pyramidev::DestructorWillUnlockShared<std::shared_mutex>>
                        access = foo.scopeAccessShared();
                    const Foo& fooRef = access.first; // en attendant les structured bindings de C++17
     
                    // code
                }
                \endcode
     */
     
    #include "StdMutexAdapter.h"
    #include "DestructorWillUnlock.h"
     
    namespace pyramidev {
     
    /*!
     * \brief   Classe qui aide à n'utiliser un objet que quand son mutex associé est bloqué.
     * \warning Si Mutex n'a pas la même syntaxe que les mutex de la STL et si MutexWrapper est
     *          StdMutexAdapter<Mutex>, alors il faut spécialiser le modèle StdMutexAdapter pour
     *          le type Mutex. Sinon, ça ne compilera pas.
     * \warning "Mutex&" doit être implicitement convertible en "MutexWrapper::mutex_type&".
     */
    template<typename T, class Mutex, class MutexWrapper = StdMutexAdapter<Mutex>>
    class ThreadShared
    {
    	ThreadShared(const ThreadShared&)            = delete;
    	ThreadShared(ThreadShared&&)                 = delete;
    	ThreadShared& operator=(const ThreadShared&) = delete;
    	ThreadShared& operator=(ThreadShared&&)      = delete;
     
    private:
    	T                    m_obj;
    	mutable MutexWrapper m_mtx;
     
    public:
    	template<typename... Args>
    	explicit ThreadShared(Mutex& mtx, Args&&... args) :
    		m_obj(std::forward<Args>(args)...),
    		m_mtx(mtx)
    	{}
     
    	~ThreadShared() {}
     
    	std::pair<T&, DestructorWillUnlockStdMutex<MutexWrapper>> scopeAccess()
    	{
    		m_mtx.lock();
    		return std::pair<T&, DestructorWillUnlockStdMutex<MutexWrapper>>(
    			m_obj, DestructorWillUnlockStdMutex<MutexWrapper>(&m_mtx));
    	}
     
    	std::pair<const T&, DestructorWillUnlockStdMutex<MutexWrapper>> scopeAccess() const
    	{
    		m_mtx.lock();
    		return std::pair<const T&, DestructorWillUnlockStdMutex<MutexWrapper>>(
    			m_obj, DestructorWillUnlockStdMutex<MutexWrapper>(&m_mtx));
    	}
     
    	std::pair<const T&, DestructorWillUnlockSharedStdMutex<MutexWrapper>> scopeAccessShared() const
    	{
    		m_mtx.lock_shared();
    		return std::pair<const T&, DestructorWillUnlockSharedStdMutex<MutexWrapper>>(
    			m_obj, DestructorWillUnlockSharedStdMutex<MutexWrapper>(&m_mtx));
    	}
     
    	/*!
             * \details Si le blocage du mutex échoue, on retourne un pointeur nul et
             *          un objet dont le destructeur ne fera rien.
             */
    	std::pair<T*, DestructorWillUnlockStdMutex<MutexWrapper>> tryScopeAccess()
    	{
    		const bool locked = m_mtx.try_lock();
    		T*            const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<T*, DestructorWillUnlockStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockStdMutex<MutexWrapper>(mtxPtr));
    	}
     
    	std::pair<const T*, DestructorWillUnlockStdMutex<MutexWrapper>> tryScopeAccess() const
    	{
    		const bool locked = m_mtx.try_lock();
    		const T*      const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<const T*, DestructorWillUnlockStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockStdMutex<MutexWrapper>(mtxPtr));
    	}
     
    	std::pair<const T*, DestructorWillUnlockSharedStdMutex<MutexWrapper>> tryScopeAccessShared() const
    	{
    		const bool locked = m_mtx.try_lock_shared();
    		const T*      const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<const T*, DestructorWillUnlockSharedStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockSharedStdMutex<MutexWrapper>(mtxPtr));
    	}
     
    	template<class Rep, class Period>
    	std::pair<T*, DestructorWillUnlockStdMutex<MutexWrapper>>
    		tryScopeAccessFor(const std::chrono::duration<Rep,Period>& timeout_duration)
    	{
    		const bool locked = m_mtx.try_lock_for(timeout_duration);
    		T*            const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<T*, DestructorWillUnlockStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockStdMutex<MutexWrapper>(mtxPtr));
    	}
     
    	template<class Rep, class Period>
    	std::pair<const T*, DestructorWillUnlockStdMutex<MutexWrapper>>
    		tryScopeAccessFor(const std::chrono::duration<Rep,Period>& timeout_duration) const
    	{
    		const bool locked = m_mtx.try_lock_for(timeout_duration);
    		const T*      const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<const T*, DestructorWillUnlockStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockStdMutex<MutexWrapper>(mtxPtr));
    	}
     
    	template<class Rep, class Period>
    	std::pair<const T*, DestructorWillUnlockSharedStdMutex<MutexWrapper>>
    		tryScopeAccessSharedFor(const std::chrono::duration<Rep,Period>& timeout_duration) const
    	{
    		const bool locked = m_mtx.try_lock_shared_for(timeout_duration);
    		const T*      const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<const T*, DestructorWillUnlockSharedStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockSharedStdMutex<MutexWrapper>(mtxPtr));
    	}
     
    	template<class Clock, class Duration>
    	std::pair<T*, DestructorWillUnlockStdMutex<MutexWrapper>>
    		tryScopeAccessUntil(const std::chrono::time_point<Clock,Duration>& timeout_time)
    	{
    		const bool locked = m_mtx.try_lock_until(timeout_time);
    		T*            const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<T*, DestructorWillUnlockStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockStdMutex<MutexWrapper>(mtxPtr));
    	}
     
    	template<class Clock, class Duration>
    	std::pair<const T*, DestructorWillUnlockStdMutex<MutexWrapper>>
    		tryScopeAccessUntil(const std::chrono::time_point<Clock,Duration>& timeout_time) const
    	{
    		const bool locked = m_mtx.try_lock_until(timeout_time);
    		const T*      const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<const T*, DestructorWillUnlockStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockStdMutex<MutexWrapper>(mtxPtr));
    	}
     
    	template<class Clock, class Duration>
    	std::pair<const T*, DestructorWillUnlockSharedStdMutex<MutexWrapper>>
    		tryScopeAccessSharedUntil(const std::chrono::time_point<Clock,Duration>& timeout_time) const
    	{
    		const bool locked = m_mtx.try_lock_shared_until(timeout_time);
    		const T*      const objPtr = locked ? &m_obj : nullptr;
    		MutexWrapper* const mtxPtr = locked ? &m_mtx : nullptr;
    		return std::pair<const T*, DestructorWillUnlockSharedStdMutex<MutexWrapper>>(
    			objPtr, DestructorWillUnlockSharedStdMutex<MutexWrapper>(mtxPtr));
    	}
    };
     
    } // namespace pyramidev
     
    #endif
    Je recopie l'exemple d'utilisation depuis mon précédent message, mais en prenant en compte les noms qui ont changé :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    // Soit deux classes Foo et Bar. Pour éviter les deadlocks,
    // posons comme convention que le mutex de Foo doit être bloqué avant celui de Bar.
    // Alors, il n'y a pas de fonction avec à la fois un paramètre
    // pyramidev::ThreadShared<Foo, std::mutex>& et un paramètre Bar&.
     
    ///////////////
    // Dans Foo.h :
     
    class Foo
    {
    private:
        bool m_work;
        // ...
    public:
        static void doA(const pyramidev::ThreadShared<Foo, std::mutex>& foo,
                              pyramidev::ThreadShared<Bar, std::mutex>& bar);
        void doA(pyramidev::ThreadShared<Bar, std::mutex>& bar) const;
        void doC(Bar& bar) const;
        // ...
    };
     
    ///////////////
    // Dans Bar.h :
     
    class Bar
    {
    private:
        // ...
    public:
        static void doB(pyramidev::ThreadShared<Bar, std::mutex>& bar,
                        const Foo& foo);
        // ...
    };
     
    /////////////////
    // Dans Foo.cpp :
     
    void Foo::doA(const pyramidev::ThreadShared<Foo, std::mutex>& foo,
                        pyramidev::ThreadShared<Bar, std::mutex>& bar)
    {
        std::pair<const Foo&, pyramidev::DestructorWillUnlock<std::mutex>> access = foo.scopeAccess();
        const Foo& fooRef = access.first; // Vivement les structured bindings de C++17 !
        fooRef.doA(bar);
    }
     
    void Foo::doA(pyramidev::ThreadShared<Bar, std::mutex>& bar) const
    {
        if(m_work) {
            Bar::doB(bar, *this);
        }
    }
     
    void Foo::doC(Bar& bar) const
    {
        // *this et bar sont protégés par un mutex bloqué chacun.
        // On peut faire ce que l'on veut avec.
    }
     
    /////////////////
    // Dans Bar.cpp :
     
    void Bar::doB(pyramidev::ThreadShared<Bar, std::mutex>& bar,
                  const Foo& foo)
    {
        std::pair<Bar&, pyramidev::DestructorWillUnlock<std::mutex>> access = bar.scopeAccess();
        Bar& barRef = access.first; // Vivement les structured bindings de C++17 !
        foo.doC(barRef);
    }
    Remarques sur l'implémentation :
    • A la place de DestructorWillUnlockStdMutex et DestructorWillUnlockSharedStdMutex, j'aurais pu utiliser directement std::unique_lock et std::shared_lock, mais ces deux modèles de classe peuvent débloquer le mutex avant le destructeur, ce que je ne veux pas autoriser à faire.
    • Les méthodes de ThreadShared retournent souvent un objet avec un nom verbeux du genre std::pair<T&, DestructorWillUnlockStdMutex<MutexWrapper>>. A la place, j'aurais pu retourner une classe perso ayant une méthode qui retourne une référence vers l'objet, mais cela n'aurait pas permis à l'avenir de profiter des structured bindings de C++17.
    • Pour ThreadShared, j'ai hésité à dégager le dernier paramètre template et à imposer sa valeur par défaut : StdMutexAdapter<Mutex>. L'avantage est que cela aurait diminué le nom des types, ce qui aurait rendu plus lisibles les messages d'erreur du compilateur. Mais j'ai finalement choisi de laisser à l'utilisateur la possibilité d'avoir plusieurs types de wrapper pour un même type de mutex.


    EDIT du 10/09/2016 vers 13h45 :
    • Modification de DestructorWillUnlock.h et ThreadShared.h pour rendre le code plus propre le cas où MutexWrapper n'est pas StdMutexAdapter<Mutex>.
    • Ajout de commentaires au début de ThreadShared.h pour expliquer à quoi sert le modèle de classe ThreadShared.

  7. #7
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 115
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 115
    Points : 32 967
    Points
    32 967
    Billets dans le blog
    4
    Par défaut
    C'est pas un peu (beaucoup trop) compliqué alors qu'il existe std::lock_guard ?
    Et pour le try_lock il y a std::try_lock.
    Tout ce que je vois c'est de l'habillage autour d'un mutex, juste de la surcharge en fait, aucune nouvelle fonctionnalité ou utilisation spécifique que je ne trouverais pas beaucoup plus simple à faire en manipulant un std::mutex et les 2 fonctions ci-dessus.
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  8. #8
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut
    Dans mon précédent message, je n'ai pas été assez explicite sur le rôle de ThreadShared.

    Le besoin initial, c'est : Comment s'assurer qu'un objet est protégé avant de le manipuler ?
    Cela amène la question : Quand on a une référence vers un objet, comment savoir si l'accès à cet objet est déjà protégé ?

    Une première solution est de documenter ou de choisir les bons noms de variable :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    void toto(Bar& bar_a_proteger, const Foo& foo_deja_protege)
    {
    	// Chercher le mutex associé à bar_a_proteger et le bloquer, par exemple avec std::lock_guard.
    }
    La solution que je propose consiste, en gros, à mettre systématiquement ensemble l'objet et la référence vers le mutex pour signaler qu'il faut protéger l'objet :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    void toto(std::pair<Bar, std::mutex&>& bar, const Foo& foo)
    {
    	// Bloquer le mutex de bar.
    	// foo n'est pas accompagné d'un mutex ici, donc on considère qu'il est déjà protégé, ou bien qu'il n'est pas partagé par plusieurs processus.
    }
    Mais, à la place de std::pair<Bar, std::mutex&>, je propose ThreadShared<Bar, std::mutex> dont l'interface oblige à bloquer le mutex pour accéder à l'objet et force le déblocage du mutex quand on sort du bloc où on a bloqué le mutex (sauf bidouille intentionnelle de l'utilisateur avec la sémantique de mouvement).

  9. #9
    Expert éminent sénior
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 275
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 275
    Points : 10 985
    Points
    10 985
    Par défaut
    C'était un article intéressant (la moitié disait des bêtises sur volatile), mais l'autre avait montré comment décorer une fonction pour ne pas mélanger des choses incompatibles.
    Je me souviens d'un autre article de Meyer sur Artima je crois qui avait montré une preuve de concept sur comment décorer n'importe quelle fonctions avec autant que couleurs que l'on veut et empêcher les mélanges invalides. C'était vers 2008, et les pauvres compilateurs avait du mal avec ça: http://www.artima.com/cppsource/codefeatures.html

    Sur le sujet de la sécurisation de ressources partagées, il ne faut pas oublier que le premier problème à éliminer est la présence d'une ressource partagée. Ensuite, quand vraiment on ne peut pas faire autrement, j'ai vu que clang introduisait des attributs pour faire de la vérification statique de code (http://clang.llvm.org/docs/ThreadSafetyAnalysis.html). Chose probablement plus pratique que la magouille avec volatile. Je me souviens qu'à l'époque j'étais resté sur ma faim avec la technique : je ne savais jamais me rappeler de l'implication d'être dans un contexte volatile.
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  10. #10
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut
    Merci pour ces deux liens intéressants, surtout le deuxième sur l'analyse statique du code avec Clang.

    Non seulement cette analyse statique vérifie les lock et unlock manquants mais, en plus, je vois qu'ils ont prévu des macros ACQUIRED_BEFORE et ACQUIRED_AFTER, hélas pas encore implémentées, pour prévenir les interblocages :
    ACQUIRED_BEFORE and ACQUIRED_AFTER are attributes on member declarations, specifically declarations of mutexes or other capabilities. These declarations enforce a particular order in which the mutexes must be acquired, in order to prevent deadlock.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Mutex m1;
    Mutex m2 ACQUIRED_AFTER(m1);
     
    // Alternative declaration
    // Mutex m2;
    // Mutex m1 ACQUIRED_BEFORE(m2);
     
    void foo() {
      m2.Lock();
      m1.Lock();  // Warning!  m2 must be acquired after m1.
      m1.Unlock();
      m2.Unlock();
    }
    [...]
    ACQUIRED_BEFORE(...) and ACQUIRED_AFTER(...) are currently unimplemented.

  11. #11
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut
    Up.

    Résumé des épisodes précédents :
    Le 1er février 2001, Andrei Alexandrescu avait publié un article dans lequel il présentait un hack avec volatile pour faire du typage fort autour de la synchronisation en programmation concurrente.
    En juillet 2016, dans le fil présent, j'avais construit une alternative : un modèle de classe ThreadShared<T, Mutex> qui encapsule un objet et une référence de mutex et qui possède des fonctions membres comme std::pair<T&, MovableLockGuard<Mutex>> scopeAccess().

    Aujourd'hui (ou plutôt hier, car minuit vient de passer), je suis tombé sur une vidéo intéressante d'une conférence de Andrei Alexandrescu :

    La vidéo a été publiée sur youtube le 8 août 2016, mais la conférence date de début juin 2016, donc avant ma publication de ThreadShared dans le fil présent.
    Dans cette vidéo, Andrei a créé une solution avec une classe Synchonized<T, Mutex> qui ressemble à mon modèle de classe ThreadShared<T, Mutex>. Synchonized<T, Mutex> contient des sous-classes LockedPtr et ConstLockedPtr dont les rôles respectifs sont similaires à ceux de std::pair<T&, std::unique_lock<Mutex>> et de std::pair<const T&, std::shared_lock<Mutex>>.

    Syntaxiquement, je trouve le modèle de classe Synchonized<T, Mutex> de Andrei Alexandrescu meilleur que mon ThreadShared<T, Mutex>.
    Cela dit, je trouve dommage qu'il ait ajouté des opérations de copie à LockedPtr et ConstLockedPtr sous le motif que Mutex peut être un mutex récursif. Le problème est que, dans le cas d'un mutex non récursif, à la place d'une erreur de compilation, on aura un self-deadlock.

    Cela dit, après avoir pratiqué davantage de programmation concurrente, je pense que le modèle de classe Synchonized<T, Mutex> de Andrei Alexandrescu et mon modèle de classe ThreadShared<T, Mutex> sont tous les deux inutilement compliqués pour la majorité des besoins.
    Je vais prendre un exemple dans lequel on pourrait se dire que Synchonized ou ThreadShared pourraient être utiles, puis donner une solution qui ne passe par aucun de ces deux modèles de classe.
    Admettons que l'on ait une classe ThreadSafeLogger qui encapsule une classe Logger :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Logger {
    public:
    	explicit Logger(const std::string& filePath);
    	std::string getFilePath() const;
    	void setFilePath(const std::string& filePath);
    	void log(const std::string& message); //!< Open log file if needed.
    	bool isOpen() const;
    	void close();
    private:
    	std::string m_filePath;
    	// ...
    };
     
    class ThreadSafeLogger {
    public:
    	// ...
    private:
    	Logger             m_logger;
    	mutable std::mutex m_mutex;
    };
    Admettons que l'on veuille appeler la fonction suivante, qui n'appartient à aucune des deux classes précédentes :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    void sendLogFileToDeveloperByMail(const std::string& filePath);
    Il faudrait bloquer le mutex, puis fermer le fichier de log s'il est encore ouvert, puis appeler la fonction qui envoie le fichier de log par courriel, puis débloquer le mutex.
    Pour des raisons de découpage des responsabilités, la classe ThreadSafeLogger ne doit pas savoir envoyer un courriel elle-même. Donc c'est l'utilisateur de la classe qui doit appeler manuellement sendLogFileToDeveloperByMail.
    Comment faire ?
    A mon avis, le plus simple est de faire comme ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    class ThreadSafeLogger {
    public:
    	// ...
    	template<class Operation>
    	auto synchronizedOperation(Operation&& op = Operation())
    		-> decltype(op(std::declval<Logger&>()))
    	{
    		std::scoped_lock lock{m_mutex};
    		return op(m_logger);
    	}
    	template<class Operation>
    	auto synchronizedOperation(Operation&& op = Operation()) const
    		-> decltype(op(std::declval<const Logger&>()))
    	{
    		std::scoped_lock lock{m_mutex};
    		return op(m_logger);
    	}
    	std::string getFilePath() const {
    		return synchronizedOperation([](const Logger& logger) {
    			return logger.getFilePath();
    		});
    	}
    	void setFilePath(const std::string& filePath) {
    		synchronizedOperation([&filePath](Logger& logger) {
    			logger.setFilePath(filePath);
    		});
    	}
    	void log(const std::string& message) {
    		synchronizedOperation([&message](Logger& logger) {
    			logger.log(message);
    		});
    	}
    	// ...
    private:
    	Logger             m_logger;
    	mutable std::mutex m_mutex;
    };
     
    // ...
     
    	// code utilisateur
    	myThreadSafeLogger.synchronizedOperation([](Logger& logger) {
    		if(logger.isOpen())
    			logger.close();
    		sendLogFileToDeveloperByMail(logger.getFilePath());
    	});
    Edit 2017-10-03-02h13 : Ajout d'un type de retour à ThreadSafeLogger::synchronizedOperation.

Discussions similaires

  1. Appel d'une méthode virtuelles
    Par BIPBIP59 dans le forum C++Builder
    Réponses: 4
    Dernier message: 24/03/2006, 14h00
  2. Méthodes virtuelle et implémentation
    Par slate dans le forum C++
    Réponses: 2
    Dernier message: 16/02/2006, 17h16
  3. méthodes virtuelles
    Par ep31 dans le forum C++
    Réponses: 2
    Dernier message: 09/11/2005, 17h21
  4. Comment l'appel à une méthode virtuelle....
    Par Blobette dans le forum C++
    Réponses: 7
    Dernier message: 07/12/2004, 13h55
  5. [C#] Méthode virtuelle
    Par jacma dans le forum Windows Forms
    Réponses: 4
    Dernier message: 07/11/2004, 08h18

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo