Si un programme C++11 exécute deux threads et que l'un d'eux lève une exception non gérée, que se passe-t-il ? Le programme entier mourra-t-il de sa belle mort ? Le thread dans lequel l'exception est levée mourra-t-il seul (et si oui, puis-je obtenir l'exception dans ce cas) ? Quelque chose d'entièrement différent ?
Réponses
Trop de publicités?Rien n'a vraiment changé. La formulation dans le n3290 est :
Si aucun gestionnaire correspondant n'est trouvé, la fonction
std::terminate()
s'appelle
Le comportement de terminate
peut être personnalisé avec set_terminate
mais :
Comportement requis : A
terminate_handler
met fin à l'exécution du programme sans revenir à l'appelant.
Ainsi, le programme se termine dans un tel cas, les autres threads ne peuvent pas continuer à fonctionner.
Puisqu'il semble y avoir un intérêt légitime pour la propagation des exceptions et que cela est légèrement au moins en rapport avec la question, voici ma suggestion : std::thread
est à considérer comme une primitive non sûre pour construire, par exemple, des abstractions de plus haut niveau. Ils sont doublement une exception risquée : si une exception se déclenche dans le fil de discussion que nous venons de lancer, tout s'écroule, comme nous l'avons montré. Mais si une exception se déclenche dans le fil de discussion qui a lancé l'action std::thread
nous pouvons potentiellement avoir des problèmes parce que std::thread
Le destructeur de l'utilisateur exige que *this
être jointes ou détachées (ou, de manière équivalente, être pas-un-fil ). Une violation de ces exigences entraîne ... un appel à std::terminate
!
Une carte codée des dangers de std::thread
:
auto run = []
{
// if an exception escapes here std::terminate is called
};
std::thread thread(run);
// notice that we do not detach the thread
// if an exception escapes here std::terminate is called
thread.join();
// end of scope
Bien sûr, certains peuvent prétendre que si nous avons simplement detach
ed chaque fil que nous lançons, nous sommes sûrs de ce deuxième point. Le problème avec ça c'est que dans certaines situations join
est la chose la plus sensée à faire. La parallélisation "naïve" de quicksort, par exemple, nécessite d'attendre que les sous-tâches soient terminées. Dans ces situations join
sert de primitive de synchronisation (un rendez-vous).
Heureusement pour nous, ces abstractions de plus haut niveau que j'ai mentionnées existent et sont fournies avec la bibliothèque standard. Elles sont std::async
, std::future
ainsi que std::packaged_task
, std::promise
et std::exception_ptr
. La version équivalente, à l'abri des exceptions, de ce qui précède :
auto run = []() -> T // T may be void as above
{
// may throw
return /* some T */;
};
auto launched = std::async(run);
// launched has type std::future<T>
// may throw here; nothing bad happens
// expression has type T and may throw
// will throw whatever was originally thrown in run
launched.get();
Et en fait, au lieu d'appeler get
dans le fil qui a appelé async
vous pouvez plutôt renvoyer la balle à un autre sujet :
// only one call to get allowed per std::future<T> so
// this replaces the previous call to get
auto handle = [](std::future<T> future)
{
// get either the value returned by run
// or the exception it threw
future.get();
};
// std::future is move-only
std::async(handle, std::move(launched));
// we didn't name and use the return of std::async
// because we don't have to