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.