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.
Push contre Pull-Prinzip
Je veux commencer par le principe de poussée.
Push-Prinzip
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();
}
Pull-Prinzip
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';
}
Attente avec et sans restriction de temps
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';
}
Notifier un ou tous les threads en attente
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.
Une petite pause estivale
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.
Formation au code propre
(carte)
#Développement #logiciels #gérer #changementSuspension #surveillée
1685515381