Développement de logiciels : gérer le changement – Suspension surveillée

Développement de logiciels : gérer le changement – Suspension surveillée

2023-05-30 12:44:00

En tant qu’abstraction importante dans le développement de logiciels modernes, les modèles fournissent une terminologie bien définie, une documentation propre et un apprentissage des meilleurs. Cet article s’attarde davantage sur les modèles de concurrence. Guarded Suspension utilise une stratégie spéciale pour faire face au changement. Elle signale quand elle a fini de se changer.

La variante de base de la suspension gardée combine un verrou avec une condition préalable qui doit être remplie. Si la condition préalable n’est pas remplie, le thread de vérification se met en veille. Le thread de vérification utilise un verrou pour éviter une condition de concurrence pouvant entraîner une course aux données ou un blocage.




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

Il existe différentes variantes de la suspension gardée :

  • Le thread en attente peut être notifié passivement du changement d’état, ou il peut activement demander le changement d’état. Cela correspond au principe push versus pull.
  • L’attente peut être avec ou sans limite de temps.
  • La notification peut être envoyée à un ou à tous les threads en attente.

Dans cet article, je ne présente que l’idée approximative derrière la suspension gardée.

Je veux commencer par le principe de poussée.

Venez souvent variable de condition ou un Future/Promesse-Paar utilisé pour synchroniser les threads. La variable de condition ou la promesse envoie la notification au thread en attente. Une promesse n’a rien notify_one- ou notify_all-Fonction membre. Généralement sans valeur set_value-Appel utilisé pour signaler une notification. Les extraits de programme suivants montrent le thread qui envoie la notification et le thread en attente.

void waitingForWork(){
    std::cout << "Worker: Waiting for work." << 'n';
    std::unique_lock lck(mutex_);
    condVar.wait(lck, []{ return dataReady; });
    doTheWork();
    std::cout << "Work done." << 'n';
}

void setDataReady(){
    {
      std::lock_guard lck(mutex_);
      dataReady = true;
    }
    std::cout << "Sender: Data is ready."  << 'n';
    condVar.notify_one();
}

void waitingForWork(std::future&& fut){

    std::cout << "Worker: Waiting for work." << std::endl;
    fut.wait();
    doTheWork();
    std::cout << "Work done." << std::endl;

}

void setDataReady(std::promise&& prom){

    std::cout << "Sender: Data is ready."  << std::endl;
    prom.set_value();

}

Au lieu d'attendre passivement le changement de statut, les développeurs peuvent également le demander activement. C++ ne supporte pas nativement ce principe d'extraction, mais il peut être utilisé avec par exemple types de données atomiques implémenter.

std::vector mySharedWork;
std::atomic dataReady(false);

void waitingForWork(){
    std::cout << "Waiting " << 'n';
    while (!dataReady.load()){                
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
    mySharedWork[1] = 2;                     
    std::cout << "Work done " << 'n';
}

void setDataReady(){
    mySharedWork = {1, 0, 3};                  
    dataReady = true;                          
    std::cout << "Data prepared" << 'n';
}

Une variable de condition et un futur ont trois fonctions membres à attendre : wait, wait_for et wait_until. Mourir wait_for-La variante prend un certain temps et la wait_until-variante un point dans le temps. Avec les différentes stratégies d'attente, le thread consommateur attend le temps indiqué dans l'exemple de code suivant steady_clock::now() + dur. L'avenir demande la valeur; si la promesse n'est pas encore prête, le futur affiche simplement son identifiant :

void producer(promise&& prom){
    cout << "PRODUCING THE VALUE 2011nn"; 
    this_thread::sleep_for(seconds(5));
    prom.set_value(2011);
}

void consumer(shared_future fut,
              steady_clock::duration dur){
    const auto start = steady_clock::now();
    future_status status= fut.wait_until(steady_clock::now() + dur);
    if ( status == future_status::ready ){
        lock_guard lockCout(coutMutex);
        cout << this_thread::get_id() << " ready => Result: " << fut.get() 
             << 'n';
    }
    else{
        lock_guard lockCout(coutMutex);
        cout << this_thread::get_id() << " stopped waiting." << 'n';
    }
    const auto end= steady_clock::now();
    lock_guard lockCout(coutMutex);
    cout << this_thread::get_id() << " waiting time: " 
         << getDifference(start,end) << " ms" << 'n';
}

notify_one réveille l'un des threads en attente, notify_all réveille tous les threads en attente. Avec notify_one il n'est pas possible de spécifier quel thread doit être réveillé. Les threads non réveillés restent à l'état d'attente. Avec un std::future cela ne peut pas arriver parce qu'il existe une relation directe entre l'avenir et la promesse. Si une relation un-à-plusieurs doit exister, un std::shared_future au lieu d'un std::future utilisé car il peut être copié.

Le programme suivant montre un flux de travail simple avec une relation un à un et un à plusieurs entre les promesses et les contrats à terme.

// bossWorker.cpp

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

int getRandomTime(int start, int end){
  
  std::random_device seed;
  std::mt19937 engine(seed());
  std::uniform_int_distribution dist(start,end);
  
  return dist(engine);
};

class Worker{
public:
  explicit Worker(const std::string& n):name(n){};
  
  void operator() (std::promise&& preparedWork, 
                   std::shared_future boss2Worker){
      
    // prepare the work and notfiy the boss
    int prepareTime= getRandomTime(500, 2000);
    std::this_thread::sleep_for(std::chrono::milliseconds(prepareTime));
    preparedWork.set_value();                                  // (5) 
    std::cout << name << ": " << "Work prepared after " 
              << prepareTime << " milliseconds." << 'n';

    // still waiting for the permission to start working
    boss2Worker.wait();
  }    
private:
  std::string name;
};

int main(){
  
  std::cout << 'n';
  
  // define the std::promise => Instruction from the boss
  std::promise startWorkPromise;

  // get the std::shared_future's from the std::promise
  std::shared_future startWorkFuture= startWorkPromise.get_future();

  std::promise herbPrepared;
  std::future waitForHerb = herbPrepared.get_future();
  Worker herb("  Herb");                                           // (1) 
  std::thread herbWork(herb, std::move(herbPrepared), startWorkFuture);
  
  std::promise scottPrepared;
  std::future waitForScott = scottPrepared.get_future();
  Worker scott("    Scott");                                      // (2) 
  std::thread scottWork(scott, std::move(scottPrepared), startWorkFuture);
  
  std::promise bjarnePrepared;
  std::future waitForBjarne = bjarnePrepared.get_future();
  Worker bjarne("      Bjarne");                                  // (3)
  std::thread bjarneWork(bjarne, std::move(bjarnePrepared), startWorkFuture);
  
  std::cout << "BOSS: PREPARE YOUR WORK.n " << 'n';
  
  // waiting for the worker 
  waitForHerb.wait(), waitForScott.wait(), waitForBjarne.wait();  // (4)
  
  // notify the workers that they should begin to work
  std::cout << "nBOSS: START YOUR WORK. n" << 'n';
  startWorkPromise.set_value();                                   // (6)
  
  herbWork.join();
  scottWork.join();
  bjarneWork.join();
   
}

L'idée centrale du programme est que le patron (thread principal) a trois travailleurs : herb (Ligne 1), scott (ligne 3) et bjarne (ligne 3). Chaque travailleur est représenté par un fil. À la ligne (4), le patron attend que tous les ouvriers aient fini de préparer leurs lots de travaux. Cela signifie que chaque travailleur envoie le message au patron après un certain temps qu'il a terminé. La notification du travailleur au patron est une relation individuelle car ils std::future utilisé (ligne 5). En revanche, l'ordre de commencer le travail est une relation un-à-plusieurs (ligne 6) du patron à ses ouvriers. Car cette notification un à plusieurs est un std::shared_future nécessaire.



Au cours des deux prochaines semaines, je prendrai une courte pause estivale et ne publierai pas d'article de blog. Mon prochain article paraîtra le 19 juin.


(carte)

Vers la page d'accueil



#Développement #logiciels #gérer #changementSuspension #surveillée
1685515381

Facebook
Twitter
LinkedIn
Pinterest

Leave a Comment

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