Bonjour, Habr! J'attire votre attention sur une traduction de l'article
«Top 20 des erreurs de multithreading C ++ et comment les éviter» de Deb Haldar.
Scène du film «La boucle du temps» (2012)Le multithreading est l'un des domaines les plus difficiles de la programmation, en particulier en C ++. Au fil des années de développement, j'ai fait de nombreuses erreurs. Heureusement, la plupart d'entre eux ont été identifiés par un examen du code et des tests. Néanmoins, certains ont en quelque sorte glissé sur le productif, et nous avons dû éditer les systèmes d'exploitation, ce qui est toujours cher.
Dans cet article, j'ai essayé de classer toutes les erreurs que je connais avec des solutions possibles. Si vous avez connaissance d'autres écueils ou avez des suggestions pour résoudre les erreurs décrites, veuillez laisser vos commentaires sous l'article.
Erreur # 1: n'utilisez pas join () pour attendre les threads d'arrière-plan avant de quitter l'application
Si vous oubliez de rejoindre le flux (
join () ) ou de le
détacher (
detach () ) (le rendre non joignable) avant la fin du programme, cela entraînera un plantage. (La traduction contiendra les mots join dans le contexte de
join () et
detach dans le contexte de
detach () , bien que ce ne soit pas tout à fait correct. En fait,
join () est le point auquel un thread d'exécution attend l'achèvement d'un autre, et aucune jointure ou fusion de threads ne se produit [traducteur de commentaires]).
Dans l'exemple ci-dessous, nous avons oublié d'exécuter
join () du thread t1 dans le thread principal:
#include "stdafx.h"
#include <iostream>
#include <thread>
using namespace std ;
void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}
int main ( )
{
thread t1 ( LaunchRocket ) ;
//t1.join(); // join-
return 0 ;
}
Pourquoi le programme s'est-il bloqué?! Parce qu'à la fin de la fonction
main () , la variable t1 est hors de portée et le destructeur de threads a été appelé. Le destructeur vérifie si le thread t1 est
joignable . Un thread peut être
joint s'il n'a pas été détaché. Dans ce cas,
std :: terminate est appelé dans son destructeur. Voici ce que fait, par exemple, le compilateur MSVC ++.
~thread ( ) _NOEXCEPT
{ // clean up
if ( joinable ( ) )
XSTD terminate ( ) ;
}
Il existe deux façons de résoudre le problème, selon la tâche:
1. Appelez
join () du thread t1 dans le thread principal:
int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. join ( ) ; // join t1,
return 0 ;
}
2. Détachez le flux t1 du flux principal, laissez-le continuer à fonctionner comme un flux «diabolisé»:
int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ; // t1
return 0 ;
}
Erreur n ° 2: essayer de joindre un fil qui a été précédemment détaché
Si, à un moment donné du travail du programme, vous avez un flux de
détachement , vous ne pouvez pas le rattacher au flux principal. C'est une erreur très évidente. Le problème est que vous pouvez détacher le flux, puis écrire quelques centaines de lignes de code et essayer de le rattacher. Après tout, qui se souvient qu'il a écrit 300 lignes en arrière, non?
Le problème est que cela ne provoquera pas d'erreur de compilation, mais que le programme plantera au démarrage. Par exemple:
#include "stdafx.h"
#include <iostream>
#include <thread>
using namespace std ;
void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}
int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ;
//..... 100 -
t1. join ( ) ; // CRASH !!!
return 0 ;
}
La solution consiste à toujours vérifier le thread pour
joinable () avant d'essayer de le joindre au thread appelant.
int main ( )
{
thread t1 ( LaunchRocket ) ;
t1. detach ( ) ;
//..... 100 -
if ( t1. joinable ( ) )
{
t1. join ( ) ;
}
return 0 ;
}
Erreur # 3: Incompréhension selon laquelle std :: thread :: join () bloque le thread d'appel d'exécution
Dans les applications réelles, vous devez souvent séparer les opérations «de longue durée» de traitement des E / S réseau ou attendre qu'un utilisateur clique sur un bouton, etc. Un appel à
join () pour de tels flux de travail (par exemple, le thread de rendu de l'interface utilisateur) peut entraîner le blocage de l'interface utilisateur. Il existe des méthodes de mise en œuvre plus adaptées.
Par exemple, dans les applications GUI, un thread de travail, une fois terminé, peut envoyer un message au thread d'interface utilisateur. Le flux d'interface utilisateur a sa propre boucle de traitement des événements tels que: déplacer la souris, appuyer sur les touches, etc. Cette boucle peut également recevoir des messages des threads de travail et y répondre sans avoir à appeler la méthode blocking
join () .
Pour cette raison, presque toutes les interactions utilisateur dans la plate-forme
WinRT de Microsoft sont rendues asynchrones et aucune alternative synchrone n'est disponible. Ces décisions ont été prises pour garantir que les développeurs utiliseront l'API qui offre la meilleure expérience utilisateur possible. Vous pouvez vous référer au manuel «
Modern C ++ et applications Windows Store » pour plus d'informations sur ce sujet.
Erreur # 4: en supposant que les arguments de la fonction de flux sont passés par référence par défaut
Les arguments de la fonction de flux sont passés par valeur par défaut. Si vous devez apporter des modifications aux arguments passés, vous devez les passer par référence à l'aide de la fonction
std :: ref () .
Sous le spoiler, des exemples d'un autre article
C ++ 11 sur le multithreading Tutorial via Q&A - Thread Management Basics (Deb Haldar) , illustrant le passage de paramètres [env. traducteur].
plus de détails:Lors de l'exécution du code:
#include "stdafx.h"
#include <string>
#include <thread>
#include <iostream>
#include <functional>
using namespace std ;
void ChangeCurrentMissileTarget ( string & targetCity )
{
targetCity = "Metropolis" ;
cout << " Changing The Target City To " << targetCity << endl ;
}
int main ( )
{
string targetCity = "Star City" ;
thread t1 ( ChangeCurrentMissileTarget, targetCity ) ;
t1. join ( ) ;
cout << "Current Target City is " << targetCity << endl ;
return 0 ;
}
Il sera affiché dans le terminal:
Changing The Target City To Metropolis
Current Target City is Star City
Comme vous pouvez le voir, la valeur de la variable targetCity reçue par la fonction appelée dans le flux par référence n'a pas changé.
Réécrivez le code en utilisant std :: ref () pour passer l'argument:
#include "stdafx.h"
#include <string>
#include <thread>
#include <iostream>
#include <functional>
using namespace std ;
void ChangeCurrentMissileTarget ( string & targetCity )
{
targetCity = "Metropolis" ;
cout << " Changing The Target City To " << targetCity << endl ;
}
int main ( )
{
string targetCity = "Star City" ;
thread t1 ( ChangeCurrentMissileTarget, std :: ref ( targetCity ) ) ;
t1. join ( ) ;
cout << "Current Target City is " << targetCity << endl ;
return 0 ;
}
Il produira:
Changing The Target City To Metropolis
Current Target City is Metropolis
Les modifications apportées dans le nouveau thread affecteront la valeur de la variable targetCity déclarée et initialisée dans la fonction principale .
Erreur # 5: Ne protégez pas les données et les ressources partagées avec une section critique (par exemple, un mutex)
Dans un environnement multithread, généralement plusieurs threads rivalisent pour les ressources et les données partagées. Souvent, cela conduit à un état incertain pour les ressources et les données, sauf lorsque leur accès est protégé par un mécanisme qui permet à un seul thread d'exécution d'effectuer des opérations sur elles à tout moment.
Dans l'exemple ci-dessous,
std :: cout est une ressource partagée avec laquelle 6 threads fonctionnent (t1-t5 + main).
#include "stdafx.h"
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
using namespace std ;
std :: mutex mu ;
void CallHome ( string message )
{
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
}
int main ( )
{
thread t1 ( CallHome, "Hello from Jupiter" ) ;
thread t2 ( CallHome, "Hello from Pluto" ) ;
thread t3 ( CallHome, "Hello from Moon" ) ;
CallHome ( "Hello from Main/Earth" ) ;
thread t4 ( CallHome, "Hello from Uranus" ) ;
thread t5 ( CallHome, "Hello from Neptune" ) ;
t1. join ( ) ;
t2. join ( ) ;
t3. join ( ) ;
t4. join ( ) ;
t5. join ( ) ;
return 0 ;
}
Si nous exécutons ce programme, nous obtenons la conclusion:
Thread 0x1000fb5c0 says Hello from Main/Earth
Thread Thread Thread 0x700005bd20000x700005b4f000 says says Thread Thread Hello from Pluto0x700005c55000Hello from Jupiter says 0x700005d5b000Hello from Moon
0x700005cd8000 says says Hello from Uranus
Hello from Neptune
En effet, cinq threads accèdent simultanément au flux de sortie dans un ordre aléatoire. Pour rendre la conclusion plus précise, vous devez protéger l'accès à la ressource partagée à l'aide de
std :: mutex .
Modifiez simplement la fonction
CallHome () afin qu'il capture le mutex avant d'utiliser
std :: cout et le libère après.
void CallHome ( string message )
{
mu. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
mu. unlock ( ) ;
}
Erreur # 6: Oubliez de libérer le verrou après avoir quitté la section critique
Dans le paragraphe précédent, vous avez vu comment protéger une section critique avec un mutex. Cependant, appeler les méthodes
lock () et
unlock () directement sur le mutex n'est pas l'option préférée car vous pouvez oublier de donner le verrou maintenu. Que se passera-t-il ensuite? Tous les autres threads qui attendent la libération de la ressource seront bloqués à l'infini et le programme peut se bloquer.
Dans notre exemple synthétique, si vous avez oublié de déverrouiller le mutex dans l'appel de la fonction
CallHome () , le premier message du flux t1 sera émis vers le flux standard et le programme plantera. Cela est dû au fait que le thread t1 a reçu un verrou mutex, et les threads restants attendent que ce verrou soit libéré.
void CallHome ( string message )
{
mu. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
//mu.unlock();
}
Ce qui suit est la sortie de ce code - le programme s'est écrasé, affichant le seul message dans le terminal, et ne s'est pas terminé:
Thread 0x700005986000 says Hello from Pluto
De telles erreurs se produisent souvent, c'est pourquoi il n'est pas souhaitable d'utiliser les méthodes
lock () / unlock () directement à partir du mutex. Utilisez
plutôt la classe de modèle
std :: lock_guard , qui utilise l'idiome
RAII pour contrôler la durée de vie du verrou. Lorsque l'objet
lock_guard est créé, il essaie de reprendre le mutex. Lorsque le programme quitte la portée de l'objet
lock_guard , le destructeur est appelé, ce qui libère le mutex.
Nous
réécrivons la fonction
CallHome () en utilisant l'objet
std :: lock_guard :
void CallHome ( string message )
{
std :: lock_guard < std :: mutex > lock ( mu ) ; //
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
} // lock_guard
Erreur n ° 7: rendre la taille de la section critique plus grande que nécessaire
Lorsqu'un thread s'exécute à l'intérieur d'une section critique, tous les autres qui tentent d'y entrer sont essentiellement bloqués. Nous devons conserver le moins d'instructions possible dans la section critique. Pour illustrer, un exemple de mauvais code avec une grande section critique est donné:
void CallHome ( string message )
{
std :: lock_guard < std :: mutex > lock ( mu ) ; // , std::cout
ReadFifyThousandRecords ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
} // lock_guard mu
La méthode
ReadFifyThousandRecords () ne modifie pas les données. Il n'y a aucune raison de l'exécuter sous verrou. Si cette méthode est exécutée pendant 10 secondes, en lisant 50 000 lignes de la base de données, tous les autres threads seront bloqués inutilement pendant toute cette période. Cela peut sérieusement affecter les performances du programme.
La bonne solution serait de ne conserver dans la section critique que le travail avec
std :: cout .
void CallHome ( string message )
{
ReadFifyThousandRecords ( ) ; // ..
std :: lock_guard < std :: mutex > lock ( mu ) ; // , std::cout
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
} // lock_guard mu
Erreur n ° 8: prendre plusieurs verrous dans un ordre différent
Il s'agit de l'une des causes les plus courantes de
blocage , une situation dans laquelle les threads sont bloqués à l'infini en raison de l'attente de l'accès aux ressources bloquées par d'autres threads. Prenons un exemple:
flux 1 | flux 2 |
---|
verrou A | serrure B |
// ... quelques opérations | // ... quelques opérations |
serrure B | verrou A |
// ... quelques autres opérations | // ... quelques autres opérations |
déverrouiller B | déverrouiller A |
déverrouiller A | déverrouiller B |
Une situation peut se produire dans laquelle le thread 1 tentera de capturer le verrou B et sera bloqué car le thread 2 l'a déjà capturé. Dans le même temps, le deuxième thread tente de capturer le verrou A, mais ne peut pas le faire, car il a été capturé par le premier thread. Le fil 1 ne peut pas libérer le verrou A tant qu'il ne verrouille pas B, etc. En d'autres termes, le programme se bloque.
Cet exemple de code vous aidera à reproduire le
blocage :
#include "stdafx.h"
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
using namespace std ;
std :: mutex muA ;
std :: mutex muB ;
void CallHome_Th1 ( string message )
{
muA. lock ( ) ;
// -
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
muB. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
muB. unlock ( ) ;
muA. unlock ( ) ;
}
void CallHome_Th2 ( string message )
{
muB. lock ( ) ;
// -
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
muA. lock ( ) ;
cout << "Thread " << this_thread :: get_id ( ) << " says " << message << endl ;
muA. unlock ( ) ;
muB. unlock ( ) ;
}
int main ( )
{
thread t1 ( CallHome_Th1, "Hello from Jupiter" ) ;
thread t2 ( CallHome_Th2, "Hello from Pluto" ) ;
t1. join ( ) ;
t2. join ( ) ;
return 0 ;
}
Si vous exécutez ce code, il se bloquera. Si vous allez plus loin dans le débogueur dans la fenêtre du thread, vous verrez que le premier thread (appelé depuis
CallHome_Th1 () ) tente d'obtenir un verrou sur le mutex B, tandis que le thread 2 (appelé depuis
CallHome_Th2 () ) tente de bloquer le mutex A. Aucun des threads ne peut pas réussir, ce qui conduit à une impasse!

(l'image est cliquable)
Que pouvez-vous y faire? La meilleure solution serait de restructurer le code afin que les verrous de verrouillage se produisent dans le même ordre à chaque fois.
Selon la situation, vous pouvez utiliser d'autres stratégies:
1. Utilisez la
classe wrapper
std :: scoped_lock pour capturer conjointement plusieurs verrous:
std :: scoped_lock lock { muA, muB } ;
2. Utilisez la
classe std :: timed_mutex , dans laquelle vous pouvez spécifier un délai d'attente, après quoi le verrou sera libéré si la ressource n'est pas devenue disponible.
std :: timed_mutex m ;
void DoSome ( ) {
std :: chrono :: milliseconds timeout ( 100 ) ;
while ( true ) {
if ( m. try_lock_for ( timeout ) ) {
std :: cout << std :: this_thread :: get_id ( ) << ": acquire mutex successfully" << std :: endl ;
m. unlock ( ) ;
} else {
std :: cout << std :: this_thread :: get_id ( ) << ": can't acquire mutex, do something else" << std :: endl ;
}
}
}
Erreur # 9: Essayer de saisir deux fois un verrou std :: mutex
Essayer de verrouiller le verrou deux fois entraînera un comportement indéfini. Dans la plupart des implémentations de débogage, cela se bloquera. Par exemple, dans le code ci-dessous,
LaunchRocket () verrouille le mutex puis appelle
StartThruster () . Ce qui est curieux, dans le code ci-dessus, vous ne rencontrerez pas ce problème pendant le fonctionnement normal du programme, le problème se produit uniquement lorsqu'une exception est levée, qui s'accompagne d'un comportement indéfini ou que le programme se termine anormalement.
#include "stdafx.h"
#include <iostream>
#include <thread>
#include <mutex>
std :: mutex mu ;
static int counter = 0 ;
void StartThruster ( )
{
try
{
// -
}
catch ( ... )
{
std :: lock_guard < std :: mutex > lock ( mu ) ;
std :: cout << "Launching rocket" << std :: endl ;
}
}
void LaunchRocket ( )
{
std :: lock_guard < std :: mutex > lock ( mu ) ;
counter ++ ;
StartThruster ( ) ;
}
int main ( )
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;
return 0 ;
}
Pour résoudre ce problème, vous devez corriger le code de manière à empêcher la nouvelle récupération des verrous reçus précédemment. Vous pouvez utiliser
std :: recursive_mutex comme solution de béquille, mais une telle solution indique presque toujours une mauvaise architecture du programme.
Erreur # 10: Utilisez des mutex lorsque les types std :: atomic sont suffisants
Lorsque vous devez modifier des types de données simples, par exemple, une valeur booléenne ou un compteur entier, l'utilisation de
std: atomic donnera généralement de meilleures performances que l'utilisation de mutex.
Par exemple, au lieu d'utiliser la construction suivante:
int counter ;
...
mu. lock ( ) ;
counter ++ ;
mu. unlock ( ) ;
Il vaut mieux déclarer une variable comme
std :: atomic :
std :: atomic < int > counter ;
...
counter ++ ;
Pour une comparaison détaillée de mutex et atomique, voir
Comparaison: programmation sans verrouillage avec atomics en C ++ 11 vs. mutex et serrures RW »Erreur # 11: créer et détruire directement un grand nombre de threads, au lieu d'utiliser un pool de threads gratuits
La création et la destruction de threads est une opération coûteuse en termes de temps processeur. Imaginez une tentative de création d'un flux pendant que le système effectue des opérations de calcul intensif, par exemple, le rendu de graphiques ou la physique des jeux informatiques. L'approche souvent utilisée pour de telles tâches consiste à créer un pool de threads pré-alloués qui peuvent gérer des tâches de routine, telles que l'écriture sur le disque ou l'envoi de données sur le réseau tout au long du cycle de vie du processus.
Un autre avantage du pool de threads par rapport à la génération et à la destruction de threads vous-même est que vous n'avez pas à vous soucier de la
sursouscription des threads (une situation dans laquelle le nombre de threads dépasse le nombre de cœurs disponibles et une partie importante du temps du processeur est consacrée à changer de contexte [env. traducteur]). Cela peut affecter les performances du système.
De plus, l'utilisation du pool nous évite d'avoir à gérer le cycle de vie des threads, ce qui se traduit finalement par un code plus compact avec moins d'erreurs.
Les deux bibliothèques les plus populaires qui implémentent le pool de threads sont
Intel Thread Building Blocks (TBB) et
Microsoft Parallel Patterns Library (PPL) .
Erreur n ° 12: ne gère pas les exceptions qui se produisent dans les threads d'arrière-plan
Les exceptions levées dans un thread ne peuvent pas être gérées dans un autre thread. Imaginons que nous ayons une fonction qui lève une exception. Si nous exécutons cette fonction dans un thread distinct se ramifiant à partir du thread principal d'exécution, et que nous attendons que nous interceptions toute exception levée à partir du thread supplémentaire, cela ne fonctionnera pas. Prenons un exemple:
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>
static std :: exception_ptr teptr = nullptr ;
void LaunchRocket ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
}
int main ( )
{
try
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;
}
catch ( const std :: exception & ex )
{
std :: cout << "Thread exited with exception: " << ex. what ( ) << " \n " ;
}
return 0 ;
}
Lorsque ce programme est exécuté, il se bloque, cependant, le bloc catch dans la fonction main () ne s'exécutera pas et ne gèrera pas l'exception levée dans le thread t1.
La solution à ce problème consiste à utiliser les fonctionnalités de C ++ 11:
std :: exception_ptr est utilisé pour gérer l'exception levée dans le thread d'arrière-plan. Voici les étapes à suivre:
- Créer une instance globale de la classe std :: exception_ptr initialisée à nullptr
- Dans une fonction qui s'exécute dans un thread séparé, gérez toutes les exceptions et définissez la valeur std :: current_exception () de la variable globale std :: exception_ptr déclarée à l'étape précédente
- Vérifier la valeur d'une variable globale à l'intérieur du thread principal
- Si la valeur est définie, utilisez la fonction std :: rethrow_exception (exception_ptr p) pour appeler à plusieurs reprises l'exception précédemment interceptée, en la passant par référence en tant que paramètre
Le rappel d'une exception par référence ne se produit pas dans le thread dans lequel elle a été créée, cette fonctionnalité est donc idéale pour gérer les exceptions dans différents threads.
Dans le code ci-dessous, vous pouvez gérer en toute sécurité l'exception levée dans le thread d'arrière-plan.
#include "stdafx.h"
#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>
static std :: exception_ptr globalExceptionPtr = nullptr ;
void LaunchRocket ( )
{
try
{
std :: this_thread :: sleep_for ( std :: chrono :: milliseconds ( 100 ) ) ;
throw std :: runtime_error ( "Catch me in MAIN" ) ;
}
catch ( ... )
{
//
globalExceptionPtr = std :: current_exception ( ) ;
}
}
int main ( )
{
std :: thread t1 ( LaunchRocket ) ;
t1. join ( ) ;
if ( globalExceptionPtr )
{
try
{
std :: rethrow_exception ( globalExceptionPtr ) ;
}
catch ( const std :: exception & ex )
{
std :: cout << "Thread exited with exception: " << ex. what ( ) << " \n " ;
}
}
return 0 ;
}
Erreur # 13: Utilisez des threads pour simuler un fonctionnement asynchrone, au lieu d'utiliser std :: async
Si vous avez besoin du code pour exécuter de manière asynchrone, c'est-à-dire sans bloquer le thread principal d'exécution, le meilleur choix serait d'utiliser
std :: async () . Cela revient à créer un flux et à passer le code nécessaire à exécuter dans ce flux via un pointeur vers une fonction ou un paramètre sous la forme d'une fonction lambda. Cependant, dans ce dernier cas, vous devez surveiller la création, l'attachement / détachement de ce thread, ainsi que la gestion de toutes les exceptions qui peuvent se produire dans ce thread. Si vous utilisez
std :: async () , vous vous libérez de ces problèmes et réduisez également fortement vos chances de vous retrouver dans une
impasse .
Un autre avantage significatif de l'utilisation de
std :: async est la possibilité de récupérer le résultat d'une opération asynchrone sur le thread appelant en utilisant l'objet
std :: future . Imaginez que nous ayons une fonction
ConjureMagic () qui retourne un int. Nous pouvons démarrer une opération asynchrone, qui définira la valeur future sur le
futur objet, une fois la tâche terminée, et nous pouvons extraire le résultat de l'exécution de cet objet dans le flux d'exécution à partir duquel l'opération a été appelée.
// future
std :: future asyncResult2 = std :: async ( & ConjureMagic ) ;
//... - future
// future
int v = asyncResult2. get ( ) ;
Récupérer le résultat du thread en cours d'exécution vers l'appelant est plus lourd. Deux voies sont possibles:
- Passer une référence à la variable de sortie au flux dans lequel elle enregistrera le résultat.
- Stockez le résultat dans la variable de champ de l'objet de workflow, qui peut être lue dès que le thread a terminé son exécution.
Kurt Guntheroth a constaté qu'en termes de performances, la surcharge de création d'un flux est 14 fois supérieure à celle de l'utilisation de l'
async .
Conclusion: utilisez
std :: async () par défaut jusqu'à ce que vous trouviez des arguments solides en faveur de l'utilisation directe de
std :: thread .
Erreur n ° 14: n'utilisez pas std :: launch :: async si l'asynchronie est requise
La fonction
std :: async () n'est pas tout à fait le bon nom, car par défaut, elle peut ne pas s'exécuter de manière asynchrone!
Il existe deux stratégies d'exécution
std :: async :
- std :: launch :: async : la fonction passée commence à s'exécuter immédiatement dans un thread séparé
- std :: launch :: deferred : la fonction passée ne démarre pas immédiatement, son lancement est retardé avant que les appels get () ou wait () ne soient effectués sur l'objet std :: future , qui sera renvoyé par l'appel std :: async . Au lieu d'appeler ces méthodes, la fonction sera exécutée de manière synchrone.
Lorsque nous appelons
std :: async () avec des paramètres par défaut, cela commence par une combinaison de ces deux paramètres, ce qui conduit en fait à un comportement imprévisible. Il existe un certain nombre d'autres difficultés associées à l'utilisation de
std: async () avec la stratégie de démarrage par défaut:
- incapacité de prédire l'accès correct aux variables de flux locales
- une tâche asynchrone peut ne pas démarrer du tout car les appels aux méthodes get () et wait () peuvent ne pas être appelés pendant l'exécution du programme
- lorsqu'elles sont utilisées dans des boucles dans lesquelles la condition de sortie s'attend à ce que l'objet std :: future soit prêt, ces boucles peuvent ne jamais se terminer, car le std :: future renvoyé par l'appel à std :: async peut démarrer dans un état différé.
Pour éviter toutes ces difficultés, appelez
toujours std :: async avec la politique de
lancement std :: launch :: async .
Ne faites pas ceci:
// myFunction std::async
auto myFuture = std :: async ( myFunction ) ;
Au lieu de cela, procédez comme suit:
// myFunction
auto myFuture = std :: async ( std :: launch :: async , myFunction ) ;
Ce point est examiné plus en détail dans le livre de Scott Meyers «C ++ efficace et moderne».
Erreur # 15: Appel de la méthode get () d'un objet std :: future dans un bloc de code dont le temps d'exécution est critique
Le code ci-dessous traite le résultat obtenu à partir de l'objet
std :: future d'une opération asynchrone. Cependant, la
boucle while sera verrouillée jusqu'à ce que l'opération asynchrone soit terminée (dans ce cas, pendant 10 secondes). Si vous souhaitez utiliser cette boucle pour afficher des informations à l'écran, cela peut entraîner des retards désagréables dans le rendu de l'interface utilisateur.
#include "stdafx.h"
#include <future>
#include <iostream>
int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 10 ) ) ;
return 8 ;
} ) ;
//
while ( true )
{
//
std :: cout << "Rendering Data" << std :: endl ;
int val = myFuture. get ( ) ; // 10
// - Val
}
return 0 ;
}
Remarque : un autre problème du code ci-dessus est qu'il essaie d'accéder à l'objet
std :: future une deuxième fois, bien que l'état de l'objet
std :: future ait été récupéré à la première itération de la boucle et n'a pas pu être récupéré.
La bonne solution serait de vérifier la validité de l'objet
std :: future avant d'appeler la méthode
get () . Ainsi, nous ne bloquons pas l'achèvement de la tâche asynchrone et n'essayons pas d'interroger l'objet
std :: future déjà extrait.
Cet extrait de code vous permet de réaliser ceci:
#include "stdafx.h"
#include <future>
#include <iostream>
int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 10 ) ) ;
return 8 ;
} ) ;
//
while ( true )
{
//
std :: cout << "Rendering Data" << std :: endl ;
if ( myFuture. valid ( ) )
{
int val = myFuture. get ( ) ; // 10
// - Val
}
}
return 0 ;
}
№16: , , , std::future::get()
, ,
std::future::get() ?
#include "stdafx.h"
#include <future>
#include <iostream>
int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
return 8 ;
} ) ;
if ( myFuture. valid ( ) )
{
int result = myFuture. get ( ) ;
}
return 0 ;
}
– !
,
get() std::future .
get() , ,
std::future .
,
std::future::get() try/catch . :
#include "stdafx.h"
#include <future>
#include <iostream>
int main ( )
{
std :: future < int > myFuture = std :: async ( std :: launch :: async , [ ] ( )
{
throw std :: runtime_error ( "Catch me in MAIN" ) ;
return 8 ;
} ) ;
if ( myFuture. valid ( ) )
{
try
{
int result = myFuture. get ( ) ;
}
catch ( const std :: runtime_error & e )
{
std :: cout << "Async task threw exception: " << e. what ( ) << std :: endl ;
}
}
return 0 ;
}
№17: std::async,
std::async() , , . , ( Xbox).
5- .
#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <thread>
using namespace std ;
void LaunchRocket ( )
{
cout << "Launching Rocket" << endl ;
}
int main ( )
{
thread t1 ( LaunchRocket ) ;
DWORD result = :: SetThreadIdealProcessor ( t1. native_handle ( ) , 5 ) ;
t1. join ( ) ;
return 0 ;
}
native_handle() std::thread ,
Win32 API . , Win32 API,
std::thread std::async() .
std :: async (), ces fonctions de base de la plateforme ne sont pas disponibles, ce qui rend cette méthode inadaptée aux tâches plus complexes.Une alternative consiste à créer std :: packaged_task et à le déplacer vers le thread d'exécution souhaité après avoir défini les propriétés du thread.Erreur n ° 18: créer beaucoup plus de threads «en cours d'exécution» que de cœurs disponibles
: «» «».
100% . , . , – - .
Les threads en attente utilisent seulement quelques cycles d'horloge sur lesquels ils sont exécutés pendant qu'ils attendent des événements système ou des E / S réseau, etc. Dans ce cas, la majeure partie du temps processeur disponible du noyau reste inutilisée. Un thread en attente peut traiter des données, tandis que les autres attendent que des événements se déclenchent - c'est pourquoi il est avantageux de distribuer plusieurs threads en attente à un cœur. La planification de plusieurs threads en attente par cœur peut fournir des performances de programme bien supérieures.Alors, comment comprendre le nombre de threads en cours d'exécution pris en charge par le système? Utilisez la méthode std :: thread :: hardware_concurrency () . Cette fonction renvoie généralement le nombre de cœurs de processeur, mais elle prend en compte les cœurs qui se comportent comme deux cœurs logiques ou plus en raison dehypertreading .. , . , , — . , (- ..), . , , , .
Erreur # 19: Utilisation du mot clé volatile pour la synchronisation
Le mot-clé volatile , avant de spécifier le type d'une variable, ne rend pas les opérations sur cette variable atomiques ou thread-safe. Ce que vous voulez probablement, c'est std :: atomic .Voir la discussion sur stackoverflow pour plus de détails.Erreur n ° 20: utilisation de l'architecture sans verrouillage sauf en cas de nécessité
-, . , (lock free), , , , . . , C ++, , , , ( , !).
C ++ , , , 10 .
, :
, , , , . , 19 , , , .
[. :
vovo4K .]