Comment puis-je propager des exceptions entre les threads?

Nous avons une fonction à laquelle un seul thread appelle (nous l’appelons le thread principal). Dans le corps de la fonction, nous générons plusieurs threads de travail pour effectuer un travail intensif sur le processeur, attendez que tous les threads se terminent, puis renvoyez le résultat sur le thread principal.

Le résultat est que l’appelant peut utiliser la fonction naïvement, et en interne, il utilisera plusieurs cœurs.

Tout va bien jusqu’ici ..

Le problème que nous avons concerne les exceptions. Nous ne voulons pas que des exceptions sur les threads de travail bloquent l’application. Nous voulons que l’appelant à la fonction puisse les attraper sur le thread principal. Nous devons intercepter les exceptions sur les threads de travail et les propager au thread principal pour qu’elles continuent à se dérouler à partir de là.

Comment peut-on le faire?

Le mieux que je puisse penser est:

  1. Catch une variété d’exceptions sur nos threads de travail (std :: exception et quelques-uns de nos propres).
  2. Enregistrez le type et le message de l’exception.
  3. Avoir une instruction switch correspondante sur le thread principal qui relance les exceptions quel que soit le type enregistré sur le thread de travail.

Cela présente l’inconvénient évident de ne prendre en charge qu’un ensemble limité de types d’exception, et nécessiterait une modification chaque fois que de nouveaux types d’exception seraient ajoutés.

C ++ 11 a introduit le type exception_ptr qui permet de transporter des exceptions entre les threads:

#include #include #include #include static std::exception_ptr teptr = nullptr; void f() { try { std::this_thread::sleep_for(std::chrono::seconds(1)); throw std::runtime_error("To be passed between threads"); } catch(...) { teptr = std::current_exception(); } } int main(int argc, char **argv) { std::thread mythread(f); mythread.join(); if (teptr) { try{ std::rethrow_exception(teptr); } catch(const std::exception &ex) { std::cerr < < "Thread exited with exception: " << ex.what() << "\n"; } } return 0; } 

Parce que dans votre cas, vous avez plusieurs threads de travail, vous devrez conserver une exception_ptr pour chacun d'eux.

Notez que exception_ptr est un pointeur partagé de type ptr, vous devrez donc garder au moins une exception_ptr pointant vers chaque exception ou elles seront libérées.

Spécifique à Microsoft: si vous utilisez les exceptions SEH (/ EHa), l'exemple de code transportera également les exceptions SEH telles que les violations d'access, ce qui peut ne pas être ce que vous voulez.

Actuellement, la seule méthode portable consiste à écrire des clauses catch pour tous les types d’exceptions que vous souhaitez transférer entre les threads, stocker les informations quelque part depuis cette clause catch, puis les utiliser ultérieurement pour renvoyer une exception. C’est l’approche adoptée par Boost.Exception .

En C ++ 0x, vous pourrez intercepter une exception avec catch(...) , puis la stocker dans une instance de std::exception_ptr utilisant std::current_exception() . Vous pouvez ensuite le renvoyer ultérieurement à partir du même thread ou d’un thread différent avec std::rethrow_exception() .

Si vous utilisez Microsoft Visual Studio 2005 ou version ultérieure, la bibliothèque de threads just :: thread C ++ 0x prend en charge std::exception_ptr . (Disclaimer: c’est mon produit).

Si vous utilisez C ++ 11, alors std::future peut faire exactement ce que vous recherchez: il peut automatiquement piéger les exceptions qui arrivent en haut du thread de travail et les transmettre au thread parent à le point que std::future::get est appelé. (Dans les coulisses, cela se passe exactement comme dans la réponse de @AnthonyWilliams; elle vient juste d’être mise en œuvre pour vous.)

L’inconvénient est qu’il n’y a pas de méthode standard pour “cesser de se soucier” d’un std::future ; même son destructeur bloquera simplement jusqu’à ce que la tâche soit terminée. [EDIT, 2017: Le comportement du destructeur de bloc est un problème que seuls les pseudo-futurs renvoyés par std::async , que vous ne devriez jamais utiliser de toute façon. Les futurs normaux ne bloquent pas leur destructeur. Mais vous ne pouvez toujours pas “annuler” les tâches si vous utilisez std::future : les tâches remplies de promesses continueront à tourner en arrière-plan même si personne n’écoute plus la réponse.] Voici un exemple pourrait clarifier ce que je veux dire:

 #include  #include  #include  #include  #include  #include  #include  bool is_prime(int n) { if (n == 1010) { puts("is_prime(1010) throws an exception"); throw std::logic_error("1010"); } /* We actually want this loop to run slowly, for demonstration purposes. */ std::this_thread::sleep_for(std::chrono::milliseconds(100)); for (int i=2; i < n; ++i) { if (n % i == 0) return false; } return (n >= 2); } int worker() { static std::atomic hundreds(0); const int start = 100 * hundreds++; const int end = start + 100; int sum = 0; for (int i=start; i < end; ++i) { if (is_prime(i)) { printf("%d is prime\n", i); sum += i; } } return sum; } int spawn_workers(int N) { std::vector> waitables; for (int i=0; i < N; ++i) { std::future f = std::async(std::launch::async, worker); waitables.emplace_back(std::move(f)); } int sum = 0; for (std::future &f : waitables) { sum += f.get(); /* may throw an exception */ } return sum; /* But watch out! When f.get() throws an exception, we still need * to unwind the stack, which means destructing "waitables" and each * of its elements. The destructor of each std::future will block * as if calling this->wait(). So in fact this may not do what you * really want. */ } int main() { try { int sum = spawn_workers(100); printf("sum is %d\n", sum); } catch (std::exception &e) { /* This line will be printed after all the prime-number output. */ printf("Caught %s\n", e.what()); } } 

J’ai juste essayé d’écrire un exemple de travail en utilisant std::thread et std::exception_ptr , mais quelque chose ne va pas avec std::exception_ptr (en utilisant libc ++) donc je ne l’ai pas encore fait fonctionner. 🙁

[EDIT, 2017:

 int main() { std::exception_ptr e; std::thread t1([&e](){ try { ::operator new(-1); } catch (...) { e = std::current_exception(); } }); t1.join(); try { std::rethrow_exception(e); } catch (const std::bad_alloc&) { puts("Success!"); } } 

Je n’ai aucune idée de ce que je faisais mal en 2013, mais je suis sûr que c’était de ma faute.]

Votre problème est que vous pouvez recevoir plusieurs exceptions, à partir de plusieurs threads, car chacun peut échouer, peut-être pour des raisons différentes.

Je suppose que le thread principal attend en quelque sorte que les threads se terminent pour récupérer les résultats, ou vérifie régulièrement la progression des autres threads, et que l’access aux données partagées est synchronisé.

Solution simple

La solution simple serait d’attraper toutes les exceptions dans chaque thread, les enregistrer dans une variable partagée (dans le thread principal).

Une fois tous les threads terminés, décidez de ce qu’il faut faire avec les exceptions. Cela signifie que tous les autres threads ont continué leur traitement, ce qui n’est peut-être pas ce que vous voulez.

Solution complexe

La solution la plus complexe est que chacun de vos threads vérifie aux points stratégiques de leur exécution, si une exception a été lancée depuis un autre thread.

Si un thread renvoie une exception, il est intercepté avant de quitter le thread, l’object exception est copié dans un conteneur du thread principal (comme dans la solution simple) et une variable booléenne partagée est définie sur true.

Et lorsqu’un autre thread teste ce booléen, il voit que l’exécution doit être abandonnée et abandonnée de manière gracieuse.

Lorsque tout le thread a abandonné, le thread principal peut gérer l’exception si nécessaire.

Une exception lancée à partir d’un thread ne peut pas être capturée dans le thread parent. Les threads ont des contextes et des stacks différents et, en général, le thread parent n’est pas obligé de restr là et d’attendre que les enfants finissent, afin qu’il puisse détecter leurs exceptions. Il n’y a tout simplement pas de place dans le code pour cette capture:

 try { start thread(); wait_finish( thread ); } catch(...) { // will catch exceptions generated within start and wait, // but not from the thread itself } 

Vous devrez intercepter les exceptions dans chaque thread et interpréter le statut de sortie des threads du thread principal pour relancer les exceptions dont vous pourriez avoir besoin.

BTW, dans les absents d’un catch dans un thread, il est spécifique à l’implémentation si le déroulement de la stack est fait, c’est-à-dire que les destructeurs de vos variables automatiques ne peuvent même pas être appelés avant l’appel de terminate. Certains compilateurs le font, mais ce n’est pas obligatoire.

Pourriez-vous sérialiser l’exception dans le thread de travail, la transmettre au thread principal, la désérialiser et la relancer? Je pense que pour que cela fonctionne, les exceptions devraient toutes dériver de la même classe (ou au moins un petit ensemble de classes avec le truc d’instruction switch à nouveau). De plus, je ne suis pas sûr qu’ils soient sérialisables, je pense juste à voix haute.

Il n’y a en effet aucun moyen générique de transmettre des exceptions d’un thread à un autre.

Si, comme il se doit, toutes vos exceptions dérivent de std :: exception, alors vous pouvez avoir une exception générale générale catch qui enverra en quelque sorte l’exception au thread principal où elle sera à nouveau lancée. Le problème étant que vous perdez le sharepoint lancement de l’exception. Vous pouvez probablement écrire un code dépendant du compilateur pour obtenir ces informations et les transmettre.

Si toutes vos exceptions n’héritent pas de std :: exception, alors vous avez des problèmes et vous devez écrire beaucoup de captures au plus haut niveau dans votre thread … mais la solution est toujours valide.

Vous devrez faire une capture générique pour toutes les exceptions dans l’ouvrier (y compris les exceptions non-std, telles que les violations d’access) et envoyer un message du thread de travail (je suppose que vous avez une sorte de messagerie en place?) thread, contenant un pointeur en direct sur l’exception, et relancez-le en créant une copie de l’exception. Ensuite, le travailleur peut libérer l’object d’origine et quitter.

Voir http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html . Il est également possible d’écrire une fonction wrapper de la fonction que vous appelez pour joindre un thread enfant, ce qui renvoie automatiquement (en utilisant boost :: rethrow_exception) toute exception émise par un thread enfant.