Nouvelles Du Monde

Développement logiciel – faire face au changement : Verrouillage

Développement logiciel – faire face au changement : Verrouillage

2023-05-23 11:04:00

Les modèles sont une abstraction importante dans le développement de logiciels modernes. Ils offrent une terminologie bien définie, une documentation propre et l’apprentissage des meilleurs. Cet article s’attarde davantage sur les modèles de concurrence. Le verrouillage est un moyen classique de protéger un état partagé et modifiable. Aujourd’hui je vous présente les deux variantes : Scoped Locking et Strategized Locking.


Rainer Grimm travaille depuis de nombreuses années en tant qu’architecte logiciel, chef d’équipe et responsable de la formation. Il aime écrire des articles sur les langages de programmation C++, Python et Haskell, mais aime aussi intervenir fréquemment lors de conférences spécialisées. Sur son blog Modernes C++, il traite intensément de sa passion pour le C++.



Une idée simple existe avec le verrouillage pour protéger une section critique. Une section critique est un morceau de code qu’un thread doit utiliser exclusivement.

Le verrouillage de portée est le concept de RAIIque sur un mutex est appliqué. Le verrouillage à portée est également connu sous le nom de blocage et de protection synchronisés. L’essence de cet idiome est de lier l’acquisition et la libération de ressources à la durée de vie d’un objet. Comme son nom l’indique, la durée de vie de l’objet est délimitée. Scoped signifie que le runtime C++ est responsable de la destruction de l’objet et donc de la libération de la ressource.

La classe ScopedLock implémenter le Scoped Locking.

// scopedLock.cpp

#include 
#include 
#include 
#include 

class ScopedLock{
  private:
    std::mutex& mut;
  public:
    explicit ScopedLock(std::mutex& m): mut(m){            // (1)
      mut.lock();                                          // (2)
      std::cout <<  "Lock the mutex: " << &mut <<   'n'; 
    }
    ~ScopedLock(){
      std::cout << "Release the mutex: " << &mut << 'n'; 
      mut.unlock();                                        // (3)
    }
};

int main(){

  std::cout << 'n';

  std::mutex mutex1;
  ScopedLock scopedLock1{mutex1};

  std::cout << "nBefore local scope" << 'n';
  {
    std::mutex mutex2;
    ScopedLock scopedLock2{mutex2};
  }                                                        // (4)
  std::cout << "After local scope" << 'n';
  
  std::cout << "nBefore try-catch block" << 'n';
  try{
    std::mutex mutex3;
    ScopedLock scopedLock3{mutex3};
    throw std::bad_alloc();
  }                                                        // (5)
  catch (std::bad_alloc& e){
    std::cout << e.what();
  }
  std::cout << "nAfter try-catch block" << 'n';
  
  std::cout << 'n';
  
}

ScopedLock obtient son mutex par référence (1). Le mutex est verrouillé dans le constructeur (2) et déverrouillé dans le destructeur (3). Grâce à l'idiome RAII, l'objet est détruit et le mutex est libéré automatiquement.

La portée de scopedLock1 se termine à la fin de main-Fonction. sera par conséquent mutex1 déverrouillé. La même chose s'applique mutex2 et mutex3. Ils sont automatiquement libérés à la fin de leur périmètre local (4 et 5). À mutex3 devient aussi le destructeur de scopedLock3 appelée lorsqu'une exception se produit. Intéressant c'est que mutex3 le souvenir de mutex2 réutilisés car les deux ont la même adresse.



Le verrouillage à portée présente les avantages et les inconvénients suivants :

Avantages:

  • Robustesse car le verrou est acquis et relâché automatiquement.

Désavantages:

  • Le curling récursif d'un std::mutex est un comportement indéfini et conduit généralement à un blocage.
  • Les verrous ne sont pas automatiquement libérés lorsque la fonction C longjmp est utilisé; longjpm n'appelle pas les destructeurs C++ d'objets délimités.

C++17 prend en charge les verrous en quatre versions. C++ en a un std::lock_guard / std::scoped_lock pour les simples et un std::unique_lock / std::shared_lock pour les cas d'utilisation avancés comme le verrouillage ou le déverrouillage explicite du mutex. Plus d'informations sur le mutex et les verrous dans mon article "Serrures".

Strategized Locking setzt gerne Scoped Locking an.

Supposons qu'un code tel qu'une bibliothèque doive également être utilisé simultanément dans différents domaines. Pour plus de sécurité, protégez les sections critiques avec un cadenas. Si la bibliothèque s'exécute maintenant dans un environnement à thread unique, un problème de performances se pose car un mécanisme de synchronisation coûteux est utilisé qui n'est pas nécessaire. C'est là qu'intervient le verrouillage stratégique : appliquer le modèle de stratégie au verrouillage. Cela signifie que vous enveloppez votre stratégie de verrouillage dans un objet et en faites un composant de votre système.

Deux méthodes typiques pour implémenter le verrouillage stratégique sont le polymorphisme d'exécution (orientation objet) ou le polymorphisme de compilation (modèles). Les deux méthodes améliorent la personnalisation et l'extension de la stratégie de verrouillage, facilitent la maintenance du système et prennent en charge la réutilisation des composants. L'implémentation du verrouillage stratégique au moment de l'exécution ou de la compilation diffère sur plusieurs aspects.

Avantages :

Polymorphisme à l'exécution

  • permet de configurer la stratégie de verrouillage lors de l'exécution, et
  • est plus facile à comprendre pour les développeurs venant d'un milieu orienté objet.

Polymorphisme à la compilation

  • n'a pas de désavantage d'abstraction et
  • a une hiérarchie plate.

Désavantages:

Polymorphisme à l'exécution

  • nécessite une indirection de pointeur supplémentaire et
  • peut avoir une hiérarchie de dérivation profonde.

Polymorphisme à la compilation

  • peut générer de longs messages d'erreur en cas d'erreur (cela change avec des concepts tels que BasicLockable en C++20).

Après cette discussion théorique, je vais implémenter le verrouillage stratégique dans les deux variantes. Dans mon exemple, le verrouillage stratégique prend en charge le verrouillage aucun, exclusif et partagé. Pour plus de simplicité, j'ai utilisé des mutex préexistants.

Le programme strategizedLockingRuntime.cpp utilise trois stratégies de verrouillage différentes.

// strategizedLockingRuntime.cpp

#include 
#include 
#include 

class Lock {                                     // (4)
public:
    virtual void lock() const = 0;
    virtual void unlock() const = 0;
};

class StrategizedLocking {
    Lock& lock;                                 // (1)
public:
    StrategizedLocking(Lock& l): lock(l){       // (2)
        lock.lock();
    }
    ~StrategizedLocking(){                      // (3)
        lock.unlock();
    }
};

struct NullObjectMutex{
    void lock(){}
    void unlock(){}
};

class NoLock : public Lock {                    // (5)
    void lock() const override {
        std::cout << "NoLock::lock: " << 'n';
        nullObjectMutex.lock();
    }
    void unlock() const override {
        std::cout << "NoLock::unlock: " << 'n';
         nullObjectMutex.unlock();
    }
    mutable NullObjectMutex nullObjectMutex;    // (10)
};

class ExclusiveLock : public Lock {             // (6)
    void lock() const override {
        std::cout << "    ExclusiveLock::lock: " << 'n';
        mutex.lock();
    }
    void unlock() const override {
        std::cout << "    ExclusiveLock::unlock: " << 'n';
        mutex.unlock();
    }
    mutable std::mutex mutex;                   // (11)
};

class SharedLock : public Lock {                // (7)
    void lock() const override {
        std::cout << "        SharedLock::lock_shared: " << 'n';
        sharedMutex.lock_shared();             // (8)
    }
    void unlock() const override {
        std::cout << "        SharedLock::unlock_shared: " << 'n';
        sharedMutex.unlock_shared();           // (9)
    }
    mutable std::shared_mutex sharedMutex;     // (12)
};

int main() {
    
    std::cout << 'n';
    
    NoLock noLock;
    StrategizedLocking stratLock1{noLock};
    
    {
        ExclusiveLock exLock;
        StrategizedLocking stratLock2{exLock};
        {
            SharedLock sharLock;
            StrategizedLocking startLock3{sharLock};
        }
    }
    
    std::cout << 'n';
    
}

La classe StrategizedLocking comporte une serrure (1). StrategizedLocking modélise le verrouillage de portée et verrouille donc le mutex dans le constructeur (2) et le libère à nouveau dans le destructeur (3). Lock (4) est une classe abstraite et définit l'interface des classes dérivées. Ce sont les classes NoLock (5), ExclusiveLock (6) et SharedLock (7). SharedLock appels lock_shared (8) et unlock_shared (9) sur son std::shared_mutex sur. Chacun de ces verrous contient l'un des mutex NullObjectMutex (dix), std::mutex (11) ou std::shared_mutex (ligne 12). NullObjectMutex est un espace réservé noop. Les mutex sont stockés comme mutable déclaré. Par conséquent, ils sont dans des fonctions membres constantes comme lock et unlock utilisable.

L'implémentation basée sur un modèle est très similaire à l'implémentation orientée objet. Au lieu d'une classe de base abstraite Lock Je définis le concept BasicLockable. Vous trouverez plus d'informations sur les concepts dans mes articles précédents : Notions.

template 
concept BasicLockable = requires(T lo) {
    lo.lock();
    lo.unlock();
};

BasicLockable requis par son paramètre de type T, qu'il a les fonctions de membre lock et unlock mis en œuvre. Par conséquent, le modèle de classe accepte StrategizedLocking ne tapez que les paramètres qui satisfont cette contrainte.

template  
class StrategizedLocking {
...

Enfin, l'implémentation basée sur un modèle suit.

// strategizedLockingCompileTime.cpp

#include 
#include 
#include 

template 
concept BasicLockable = requires(T lo) {
    lo.lock();
    lo.unlock();
};
    
template 
class StrategizedLocking {
    Lock& lock;
public:
    StrategizedLocking(Lock& l): lock(l){
        lock.lock();
    }
    ~StrategizedLocking(){
        lock.unlock();
    }
};

struct NullObjectMutex {
    void lock(){}
    void unlock(){}
};

class NoLock{
public:
    void lock() const {
        std::cout << "NoLock::lock: " << 'n';
        nullObjectMutex.lock();
    }
    void unlock() const {
        std::cout << "NoLock::unlock: " << 'n';
         nullObjectMutex.lock();
    }
    mutable NullObjectMutex nullObjectMutex;
};

class ExclusiveLock {
public:
    void lock() const {
        std::cout << "    ExclusiveLock::lock: " << 'n';
        mutex.lock();
    }
    void unlock() const {
        std::cout << "    ExclusiveLock::unlock: " << 'n';
        mutex.unlock();
    }
    mutable std::mutex mutex;
};

class SharedLock {
public:
    void lock() const {
        std::cout << "        SharedLock::lock_shared: " << 'n';
        sharedMutex.lock_shared();
    }
    void unlock() const {
        std::cout << "        SharedLock::unlock_shared: " << 'n';
        sharedMutex.unlock_shared();
    }
    mutable std::shared_mutex sharedMutex;
};

int main() {

    std::cout << 'n';
    
    NoLock noLock;
    StrategizedLocking stratLock1{noLock};
    
    {
        ExclusiveLock exLock;
        StrategizedLocking stratLock2{exLock};
        {
            SharedLock sharLock;
            StrategizedLocking startLock3{sharLock};
        }
    }
    
    std::cout << 'n';

}

Les programmes strategizedLockingRuntime.cpp et strategizedLockingCompileTime.cpp produire la même sortie :



Guarded Suspension utilise une stratégie différente pour faire face au changement. Il signale quand le changement a eu lieu. Dans mon prochain article, j'entrerai plus en détail sur la suspension surveillée.


(rme)

Vers la page d'accueil



#Développement #logiciel #faire #face #changement #Verrouillage
1684861095

Facebook
Twitter
LinkedIn
Pinterest

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

ADVERTISEMENT