Modèles d’architecture logicielle : Monitor Object

Modèles d’architecture logicielle : Monitor Object

2023-06-26 16:42:00

Les modèles sont une abstraction importante dans le développement de logiciels modernes et l’architecture logicielle. Ils offrent une terminologie bien définie, une documentation propre et l’apprentissage des meilleurs. Das Buch “Architecture logicielle orientée modèle : modèles pour les objets simultanés et en réseau” définit un objet moniteur comme suit : “Le modèle de conception d’objet de surveillance synchronise l’exécution simultanée des fonctions membres pour garantir qu’une seule fonction membre à la fois s’exécute dans un objet. Il permet également aux fonctions membres de l’objet de programmer leurs séquences d’exécution de manière coopérative.”

Publicité


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++.



Ainsi, le modèle de conception Monitor Object synchronise l’exécution simultanée des fonctions membres pour garantir qu’une seule fonction membre s’exécute dans un objet à la fois. Il permet également aux fonctions membres d’un objet de programmer ensemble leurs séquences d’exécution.

Aussi connu sous le nom

  • Objet passif thread-safe

Lorsque plusieurs threads accèdent à un objet partagé en même temps, les défis sont les suivants :

Publicité

  • En raison de l’accès simultané, l’objet partagé doit être protégé contre les opérations de lecture et d’écriture non synchronisées pour éviter les courses de données.
  • La synchronisation nécessaire doit faire partie de l’implémentation et non de l’interface.
  • Lorsqu’un thread a terminé avec l’objet partagé, une notification doit être déclenchée afin que le thread suivant puisse utiliser l’objet partagé. Ce mécanisme améliore les performances du système.
  • Après l’exécution d’une fonction membre, les invariants de l’objet partagé doivent être préservés.

Solution

Un client (thread) peut accéder aux fonctions membres synchronisées de l’objet Monitor et, en raison du verrouillage du moniteur, une seule fonction membre synchronisée peut être exécutée à un moment donné. Chaque objet moniteur a une condition de moniteur qui notifie les clients en attente.



  • Objet moniteur: L’objet Monitor prend en charge une ou plusieurs fonctions membres. Chaque client doit accéder à l’objet via ces fonctions membres, et chaque fonction membre s’exécute dans le thread du client.
  • Fonctionnalités de membre synchronisées: Les fonctions membres synchronisées sont les fonctions membres prises en charge par l’objet moniteur. Une seule fonction membre peut être en cours d’exécution à un moment donné. Le Thread-Safe-Interface permet de faire la distinction entre les fonctions membres qui représentent l’interface (fonctions membres synchronisées) et les fonctions membres qui représentent l’implémentation de l’objet moniteur.
  • Verrouillage du moniteur: chaque objet moniteur possède un verrou de moniteur qui garantit qu’au plus un client peut accéder à l’objet moniteur à tout moment.
  • surveiller l’état: La condition Monitor permet à des threads distincts de planifier leurs appels aux fonctions membres sur l’objet Monitor. Lorsque le client actuel a fini d’appeler les fonctions membres synchronisées, le client suivant en attente est réveillé pour appeler les fonctions membres synchronisées de l’objet moniteur.

Alors que le verrouillage du moniteur garantit un accès exclusif aux fonctions membres synchronisées, la condition du moniteur garantit une latence minimale du client. Essentiellement, le verrouillage du moniteur protège contre les courses de données et le moniteur de condition contre les interblocages.

comportement dynamique

L’interaction entre l’objet moniteur et ses composants se déroule en différentes phases.

  1. Lorsqu’un client appelle une fonction membre synchronisée sur un objet moniteur, il doit d’abord verrouiller le verrou de moniteur global. Une fois le verrouillage réussi, il exécute la fonction de membre synchronisé et déverrouille enfin le moniteur. Si le verrouillage échoue, le client est bloqué.
  2. Si le client est bloqué, il attend que la condition de surveillance envoie une notification. Cette notification se produit lorsque le verrouillage du moniteur est relâché. La notification peut maintenant être envoyée à un ou à tous les clients en attente. Attendre signifie généralement dormir en économisant les ressources, contrairement à une attente occupée.
  3. Lorsqu’un client reçoit la notification de reprise du travail, il applique le verrouillage du moniteur et exécute la fonction membre synchronisée. Le verrouillage du moniteur est à nouveau déverrouillé à la fin de la fonction synchronisée. La condition de surveillance envoie une notification pour signaler que le client suivant peut exécuter sa fonction de membre synchronisé.

Quels sont les avantages et les inconvénients de l’objet Monitor ?

Avantages

  • Le client n’est pas conscient de la synchronisation implicite de l’objet moniteur et la synchronisation est complètement encapsulée dans l’implémentation.
  • Enfin, les fonctions membres synchronisées appelées sont automatiquement planifiées. Le mécanisme de notification/maintenance des conditions de surveillance se comporte comme un simple ordonnanceur.

Désavantages

  • Il est généralement assez difficile de modifier le mécanisme de synchronisation des fonctions membres synchronisées car la fonctionnalité et la synchronisation sont étroitement liées.
  • Si une fonction membre synchronisée appelle directement ou indirectement le même objet moniteur, un blocage peut se produire.

L’exemple suivant définit un ThreadSafeQueue.

// monitorObject.cpp

#include 
#include 
#include 
#include 
#include 
#include 
#include 

class Monitor {
public:
    void lock() const {
        monitMutex.lock();
    }

    void unlock() const {
        monitMutex.unlock();
    }

    void notify_one() const noexcept {
        monitCond.notify_one();
    }

    template 
    void wait(Predicate pred) const {                 // (10)
        std::unique_lock monitLock(monitMutex);
        monitCond.wait(monitLock, pred);
    }
    
private:
    mutable std::mutex monitMutex;
    mutable std::condition_variable monitCond;
};

template                                   // (1)
class ThreadSafeQueue: public Monitor {
 public:
    void add(T val){ 
        lock();
        myQueue.push(val);                             // (6)
        unlock();
        notify_one();
    }
    
    T get(){ 
        wait( [this] { return ! myQueue.empty(); } );  // (2)
        lock();
        auto val = myQueue.front();                    // (4)
        myQueue.pop();                                 // (5)
        unlock();
        return val;
    }

private:
    std::queue myQueue;                            // (3)
};


class Dice {
public:
    int operator()(){ return rand(); }
private:
    std::function rand = 
    std::bind(std::uniform_int_distribution<>(1, 6), 
              std::default_random_engine());
};


int main(){
    
    std::cout << 'n';
    
    constexpr auto NumberThreads = 10;
    
    ThreadSafeQueue safeQueue;                      // (7)

    auto addLambda = [&safeQueue](int val){ 
      safeQueue.add(val);                                // (8)
      std::cout << val << " "
        << std::this_thread::get_id() << "; "; 
    }; 
    auto getLambda = [&safeQueue]{ safeQueue.get(); };  // (9)

    std::vector addThreads(NumberThreads);
    Dice dice;
    for (auto& thr: addThreads) thr = 
      std::thread(addLambda, dice() );

    std::vector getThreads(NumberThreads);
    for (auto& thr: getThreads) thr = std::thread(getLambda);

    for (auto& thr: addThreads) thr.join();
    for (auto& thr: getThreads) thr.join();
    
    std::cout << "nn";
     
}

L'idée centrale de l'exemple est que l'objet moniteur est encapsulé dans une classe et peut donc être réutilisé. La classe Monitor utilise un std::mutex comme un verrou de moniteur et un std :: condition_variable comme condition de surveillance. La classe Monitor fournit l'interface minimale qu'un objet moniteur doit prendre en charge.

ThreadSafeQueue étendu en (1). std::queue afin d'avoir une interface thread safe. ThreadSafeQueue dérive de la classe Monitor et utilise leurs fonctions membres pour obtenir les fonctions membres synchronisées add et get soutenir. Les fonctions de membre add et get utilisez le verrou du moniteur pour protéger l'objet du moniteur. Cela est particulièrement vrai pour ceux qui ne sont pas thread-safe myQueue. add avertit le fil d'attente lorsqu'un nouvel élément arrive myQueue était ajouté. Cette notification est thread-safe. La fonction membre get (3) mérite plus d'attention. Premièrement la wait- Fonction membre appelée de la variable de condition sous-jacente. Ce wait-Call nécessite un prédicat supplémentaire pour se protéger contre les réveils perdus et intempestifs (C++ Core Guidelines : Soyez conscient des dangers des variables conditionnelles) protéger. Les opérations pour changer le myQueue (4) et (5) doivent également être protégés car ils traitent de l'appel myQueue.push(val) (6) peuvent se chevaucher. L'objet moniteur safeQueue (7) utilise les fonctions lambda dans (8) et (9) pour obtenir un nombre à partir du safeQueue ajouter ou retirer. ThreadSafeQueue lui-même est un modèle de classe et peut prendre des valeurs de n'importe quel type. Une centaine de clients ajoutent le safeQueue 100 numéros aléatoires entre 1 et 6 (ligne 7) tandis que cent clients sélectionnent ces 100 numéros en même temps dans la safeQueue supprimé. La sortie du programme affiche les nombres et les ID de thread.



Avec C++20, le programme peut monitorObject.cpp être encore amélioré. J'ajoute d'abord l'en-tête et utiliser le concept std::predicate en tant que paramètre de modèle restreint dans le modèle de fonction wait (dix). Das Concept std::predicate garantit que le modèle de fonction wait ne peut être instancié qu'avec un prédicat. Les prédicats sont des callables qui renvoient une valeur booléenne en conséquence.

template 
void wait(Predicate pred) const {
    std::unique_lock monitLock(monitMutex);
    monitCond.wait(monitLock, pred);
}

Deuxièmement, j'utilise std :: jthread au lieu de std::thread. std::jthread est une amélioration std::thread en C++20, automatiquement dans son destructeur join appels si nécessaire.

int main() {
    
    std::cout << 'n';
    
    constexpr auto NumberThreads = 100;
    
    ThreadSafeQueue safeQueue;

    auto addLambda = [&safeQueue](int val){ 
      safeQueue.add(val);
      std::cout << val << " "
      << std::this_thread::get_id() << "; "; 
    }; 
    auto getLambda = [&safeQueue]{ safeQueue.get(); };

    std::vector addThreads(NumberThreads);
    Dice dice;
    for (auto& thr: addThreads) thr = 
      std::jthread(addLambda, dice());

    std::vector getThreads(NumberThreads);
    for (auto& thr: getThreads) thr = std::jthread(getLambda);
    
    std::cout << "nn";
     
}

Le Objet actif et l'objet moniteur sont similaires mais diffèrent de quelques manières importantes. Les deux modèles architecturaux synchronisent l'accès à un objet commun. Les fonctions membres d'un objet actif s'exécutent sur un thread différent, mais les fonctions membres de l'objet Monitor s'exécutent sur le même thread. L'objet actif dissocie mieux l'invocation des fonctions membres de l'exécution des fonctions membres et est donc plus facile à maintenir.

Complet! J'ai environ 50 articles aussi modèles de conception écrit. Dans mes prochains articles, j'écrirai sur une fonctionnalité assez inconnue de C++17, approfondirai C++20 et présenterai le nouveau standard C++ à venir, C++23. Je commence ce voyage avec C++23.


(rme)

Vers la page d'accueil



#Modèles #darchitecture #logicielle #Monitor #Object
1687859117

Facebook
Twitter
LinkedIn
Pinterest

Leave a Comment

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