Nouvelles Du Monde

Modèles d’architecture logicielle : l’objet actif

Modèles d’architecture logicielle : l’objet actif

2023-06-19 14:46: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. Celle appartenant aux modèles de concurrence Active Object décrit une entrée Wikipedia comme suit: “Le modèle de conception d’objet actif dissocie l’exécution de la méthode de l’invocation de la méthode pour les objets qui résident chacun dans leur propre thread de contrôle. L’objectif est d’introduire la concurrence, en utilisant l’invocation de méthode asynchrone et un planificateur pour gérer les demandes..”

L’objet actif dissocie l’appel de méthode de l’exécution de la méthode. L’appel de méthode s’exécute sur le thread client, mais l’exécution de la méthode s’exécute sur l’objet actif. L’objet actif a son thread et une liste d’objets de requête de méthode (requêtes courtes) à exécuter. L’appel de méthode du client met les requêtes en file d’attente dans la liste de l’objet actif. Les requêtes sont transmises au serveur.




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

L’objet actif est également connu sous le nom d’objet de concurrence

Lorsque de nombreux threads accèdent à un objet commun de manière synchronisée, les défis suivants doivent être résolus :

  • Un thread qui appelle une fonction membre gourmande en ressources de traitement ne doit pas bloquer trop longtemps les autres threads qui appellent le même objet.
  • L’accès à un objet partagé doit être facile à synchroniser.
  • Les propriétés de simultanéité des requêtes exécutées doivent être adaptables au matériel et aux logiciels spécifiques.
  • L’appel de méthode du client va à un proxy qui représente l’interface de l’objet actif.
  • Le serveur implémente ces fonctions membres et s’exécute dans le thread de l’objet actif. Lors de l’exécution, le proxy convertit l’appel en un appel de méthode au serveur. Le planificateur inscrit cette demande dans une liste d’activation.
  • La boucle d’événements du planificateur s’exécute dans le même thread que le serveur, récupérant les demandes de la liste d’activation, les supprimant et les envoyant au serveur.
  • Le client reçoit le résultat de l’appel de méthode via un futur du proxy.

Le modèle d’objet actif se compose de six composants :

  • Le adjoint (proxy) fournit une interface pour les fonctions membres accessibles de l’objet actif. L’adjoint initie la création d’une demande dans la liste d’activation. Le proxy s’exécute sur le thread client.
  • Mourir classe de demande de méthode (requête de méthode) définit l’interface de la méthode exécutée sur un objet actif. Cette interface contient également des méthodes de garde qui indiquent si le travail est prêt à être exécuté. La demande contient toutes les informations de contexte à utiliser ultérieurement.
  • Mourir liste d’activation (liste d’activation) gère les demandes en attente. La liste d’activation dissocie le thread du client du thread de l’objet actif. Le proxy insère l’objet de requête et le planificateur le supprime à nouveau. Par conséquent, l’accès à la liste d’activation doit être sérialisé.
  • Le Planificateur s’exécute dans le thread de l’objet actif et décide quelle demande de la liste d’activation sera exécutée ensuite. Le planificateur évalue les gardes de la demande pour déterminer si la demande peut être exécutée.
  • Le Serveur implémente l’objet actif et s’exécute dans le thread de l’objet actif. Le serveur implémente l’interface de demande de méthode et le planificateur appelle ses fonctions membres.
  • Le député crée le Avenir. Cela n’est nécessaire que si la requête renvoie un résultat. Le client reçoit donc le futur et peut récupérer le résultat de l’appel de méthode sur l’Objet Actif. Le client peut attendre le résultat ou l’interroger.

Le comportement dynamique de l’objet actif se compose de trois phases :

  1. Création et planification de la demande: Le client appelle une méthode du délégué. Le proxy crée une demande et la transmet au planificateur. Le planificateur met la demande en file d’attente dans la liste d’activation. De plus, le proxy renvoie un futur au client si la requête renvoie un résultat.
  2. Exécution de la fonction membre: Le planificateur détermine quelle demande devient exécutable en évaluant la méthode de garde de la demande. Il supprime la demande de la liste d’activation et l’envoie au serveur.
  3. achèvement: Si la requête renvoie un résultat, il est stocké dans le futur. Le client peut demander le résultat. Lorsque le client a le résultat, la demande et le futur peuvent être supprimés.

La figure ci-dessous montre la séquence des messages.



Avant de présenter un exemple minimal de l’objet actif, voici une brève liste de ses avantages et inconvénients :

  • La synchronisation est requise uniquement sur le thread de l’objet actif, pas sur les threads du client.
  • Séparation claire entre le client (utilisateur) et le serveur (implémenteur). Les défis de la synchronisation sont du côté de l’exécutant.
  • Augmentation du débit du système grâce à l’exécution asynchrone des requêtes. L’appel de fonctions membres gourmandes en ressources de traitement ne bloque pas l’ensemble du système.
  • Le planificateur peut utiliser différentes stratégies pour exécuter les demandes en attente. Si tel est le cas, les travaux peuvent être exécutés dans un ordre différent de celui dans la file d’attente.
  • Si les requêtes sont trop fines, les performances du modèle d’objet actif, telles que le proxy, la liste d’activation et le planificateur, peuvent être excessives.
  • En raison de la stratégie du planificateur et de la planification du système d’exploitation, le dépannage dans l’objet actif est souvent difficile. Cela est particulièrement vrai lorsque l’ordre d’exécution de l’ordre diffère de celui de la création de l’ordre.

L’exemple suivant montre une implémentation simplifiée de l’objet actif. En particulier, je ne définis pas d’interface pour les demandes de méthode à l’objet actif que le proxy et le serveur doivent implémenter. En outre, le planificateur exécute le travail suivant à la demande et la fonction membre d’exécution de l’objet actif crée les threads.

Les types de données concernés future>>> sont souvent assez longs. Pour améliorer la lisibilité, je me suis fortement appuyé sur l’utilisation de déclarations (ligne 1). Cet exemple suppose une solide compréhension des promesses et des futurs en C++. Dans mes articles sur Tâches plus de détails peuvent être trouvés.

// activeObject.cpp

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

using std::async;                                      // (1)
using std::boolalpha;
using std::cout;
using std::deque;
using std::distance;
using std::for_each;
using std::find_if;
using std::future;
using std::lock_guard;
using std::make_move_iterator;
using std::make_pair;
using std::move;
using std::mt19937;
using std::mutex;
using std::packaged_task;
using std::pair;
using std::random_device;
using std::sort;
using std::jthread;
using std::uniform_int_distribution;
using std::vector;

class IsPrime {                                         // (8)
 public:
  IsPrime(int num): numb{num} {} 
  pair operator()() {
    for (int j = 2; j * j <= numb; ++j){
      if (numb % j == 0) return make_pair(false, numb);
    }
    return make_pair(true, numb);
  }
 private:
    int numb;       
};

class ActiveObject {
 public:
    
  future> enqueueTask(int i) {
    IsPrime isPrime(i);
    packaged_task()> newJob(isPrime);
    auto isPrimeFuture = newJob.get_future();
    {
      lock_guard lockGuard(activationListMutex);
      activationList.push_back(move(newJob));            // (6)
    }
    return isPrimeFuture;
  }

  void run() {
    std::jthread j([this] {                               // (12)
      while ( !runNextTask() );                           // (13)
    });
  }

 private:

  bool runNextTask() {                                     // (14)
    lock_guard lockGuard(activationListMutex);
    auto empty = activationList.empty();
    if (!empty) {                                           // (15)
      auto myTask= std::move(activationList.front());
      activationList.pop_front();
      myTask();
    }
    return empty;
  }

  deque()>> activationList;      //(7)
  mutex activationListMutex;
};

vector getRandNumbers(int number) {
  random_device seed;
  mt19937 engine(seed());
  uniform_int_distribution<> dist(1'000'000, 1'000'000'000);  // (4)
  vector numbers;
  for (long long i = 0 ; i < number; ++i) numbers.push_back(dist(engine)); 
  return numbers;
}

future>>> getFutures(ActiveObject& activeObject, 
                                                   int numberPrimes) {
  return async([&activeObject, numberPrimes] {
    vector>> futures;
    auto randNumbers = getRandNumbers(numberPrimes);      // (3)
    for (auto numb: randNumbers){
      futures.push_back(activeObject.enqueueTask(numb));  // (5)
    }
    return futures;
  });
}
    

int main() {
    
  cout << boolalpha << 'n';
    
  ActiveObject activeObject;
        
  // a few clients enqueue work concurrently                  // (2)
  auto client1 = getFutures(activeObject, 1998);
  auto client2 = getFutures(activeObject, 2003);
  auto client3 = getFutures(activeObject, 2011);
  auto client4 = getFutures(activeObject, 2014);
  auto client5 = getFutures(activeObject, 2017);
    
  // give me the futures                                      // (9)
  auto futures = client1.get();
  auto futures2 = client2.get();
  auto futures3 = client3.get();
  auto futures4 = client4.get();
  auto futures5 = client5.get();
    
  // put all futures together                                 // (10)
  futures.insert(futures.end(),make_move_iterator(futures2.begin()), 
                               make_move_iterator(futures2.end()));
    
  futures.insert(futures.end(),make_move_iterator(futures3.begin()), 
                               make_move_iterator(futures3.end()));
    
  futures.insert(futures.end(),make_move_iterator(futures4.begin()), 
                               make_move_iterator(futures4.end()));
    
  futures.insert(futures.end(),make_move_iterator(futures5.begin()), 
                               make_move_iterator(futures5.end()));
        
  // run the promises                                         // (11)
  activeObject.run();
    
  // get the results from the futures
  vector> futResults;
  futResults.reserve(futures.size());
  for (auto& fut: futures) futResults.push_back(fut.get());   // (16)
    
  sort(futResults.begin(), futResults.end());                 // (17)
    
  // separate the primes from the non-primes
  auto prIt = find_if(futResults.begin(), futResults.end(), 
                      [](pair pa){ return pa.first == true; });
 
  cout << "Number primes: " << distance(prIt, futResults.end()) << 'n';     // (19)
  cout << "Primes:" << 'n';
  for_each(prIt, futResults.end(), [](auto p){ cout << p.second << " ";} );  // (20)
    
  cout << "nn";
    
  cout << "Number no primes: " << distance(futResults.begin(), prIt) << 'n'; // (18)
  cout << "No primes:" << 'n';
  for_each(futResults.begin(), prIt, [](auto p){ cout << p.second << " ";} );
    
  cout << 'n';
    
}

L'idée de base de l'exemple est que les clients peuvent mettre en file d'attente des tâches dans la liste d'activation en même temps. Le serveur détermine quels nombres sont premiers et la liste d'activation fait partie de l'objet actif. L'objet actif exécute les travaux qui se trouvent dans la liste d'activation dans un thread séparé et les clients peuvent interroger les résultats.

Les cinq clients placent le travail (ligne 2) au-dessus de la fonction getFutures dans la file d'attente de activeObjects. getFutures prend ça activeObject et un nombre numberPrimes contrairement à. numberPrimes génère des nombres aléatoires (ligne 3) entre 1'000'000 et 1'000'000'000 (ligne 4) et le pousse jusqu'à la valeur de retour : vector>. future contient un bool et un int. Le bool indique si le nombre est un nombre premier. Regardons de plus près la ligne (5): futures.push_back(activeObject.enqueueTask(numb)). Cet appel déclenche une nouvelle tâche à inscrire dans la liste d'activation (ligne 6). Tous les appels de la liste d'activation doivent être protégés. La liste d'activation est une deque de Promesses (ligne 7): deque()>>. Chaque promesse porte l'objet fonction lorsqu'elle est invoquée IsPrime (ligne 8) éteint. La valeur de retour est une paire de un bool et une int. Le bool indique si le nombre int est un nombre premier.

Maintenant que les lots de travaux sont préparés, le calcul peut commencer. Tous les clients renvoient leurs poignées aux contrats à terme associés à la ligne (9). Résumer tous les contrats à terme (ligne 10) facilite le travail. L'appel activeObject.run() à la ligne (11) l'exécution commence. La fonction membre run (ligne 12) crée le thread pour utiliser la fonction membre runNextTask (ligne 13) à exécuter. runNextTask (ligne 14) détermine si le deque n'est pas vide (ligne 15) et crée la nouvelle tâche. En appelant futResults.push_back(fut.get()) (ligne 16) sur chaque futur tous les résultats sont demandés et sur futResults poussé. La ligne (17) trie le vecteur de paires : vector>. Le calcul est affiché dans les lignes restantes. l'itérateur prIt à la ligne (18) contient le premier itérateur vers une paire qui a un nombre premier.

La capture d'écran ci-dessous montre le nombre de nombres premiers distance(prIt, futResults.end()) (ligne 19) et les nombres premiers (ligne 20). Seuls les premiers nombres non premiers sont affichés :



L'objet actif et l'objet moniteur synchronisent et planifient l'invocation des fonctions membres. La principale différence est que l'objet actif exécute sa fonction membre sur un thread différent, tandis que l'objet moniteur se trouve sur le même thread que le client. Dans mon prochain article, je présenterai l'objet Monitor plus en détail.


(carte)

Vers la page d'accueil



#Modèles #darchitecture #logicielle #lobjet #actif
1687201546

Facebook
Twitter
LinkedIn
Pinterest

Leave a Comment

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

ADVERTISEMENT