web-dev-qa-db-fra.com

Comment puis-je propager des exceptions entre les threads?

Nous avons une fonction dans 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, attendons la fin de tous les threads, puis renvoyons 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'à présent ..

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

Comment pouvons-nous faire cela?

Le mieux que je puisse penser est:

  1. Attrapez une variété d'exceptions sur nos threads de travail (std :: exception et quelques-uns des nôtres).
  2. Enregistrez le type et le message de l'exception.
  3. Avoir une instruction switch correspondante sur le thread principal qui renvoie les exceptions de tout type enregistrées sur le thread de travail.

Cela présente l'inconvénient évident de ne prendre en charge qu'un ensemble limité de types d'exceptions et devrait être modifié chaque fois que de nouveaux types d'exceptions sont ajoutés.

102
pauldoo

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

#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>

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 en conserver un exception_ptr pour chacun d'eux.

Notez que exception_ptr est un pointeur de type ptr partagé, vous devrez donc conserver au moins un exception_ptr pointant sur chaque exception ou elles seront publiées.

Spécifique à Microsoft: si vous utilisez des exceptions SEH (/EHa), l'exemple de code transportera également les exceptions SEH comme les violations d'accès, qui peuvent ne pas être ce que vous voulez.

73
Gerardo Hernandez

Actuellement, la seule façon portable est d'écrire des clauses catch pour tous les types d'exceptions que vous aimeriez transférer entre les threads, de stocker les informations quelque part catch, puis l'utiliser plus tard 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 En utilisant std::current_exception(). Vous pouvez ensuite le récupérer 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, alors la bibliothèque de threads juste :: thread C++ 0x prend en charge std::exception_ptr. (Avertissement: ceci est mon produit).

73
Anthony Williams

Si vous utilisez C++ 11, alors std::future peut faire exactement ce que vous recherchez: il peut automatiquement intercepter les exceptions qui arrivent en haut du thread de travail et les transmettre au thread parent au moment où std::future::get est appelé. (Dans les coulisses, cela se produit exactement comme dans la réponse de @AnthonyWilliams; il vient d'être déjà implémenté pour vous.)

L'inconvénient est qu'il n'y a pas de moyen standard de "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 de blocage-destructeur est une erreur uniquement des 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: la ou les tâches remplies de promesses continueront de se dérouler dans les coulisses même si personne n'écoute plus la réponse.] Voici un exemple de jouet qui pourrait clarifier ce que je veux dire:

#include <atomic>
#include <chrono>
#include <exception>
#include <future>
#include <thread>
#include <vector>
#include <stdio.h>

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<int> 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<std::future<int>> waitables;
    for (int i=0; i < N; ++i) {
        std::future<int> f = std::async(std::launch::async, worker);
        waitables.emplace_back(std::move(f));
    }

    int sum = 0;
    for (std::future<int> &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 similaire 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.]

10
Quuxplusone

Votre problème est que vous pourriez recevoir plusieurs exceptions, provenant de plusieurs threads, car chacune pourrait échouer, peut-être pour des raisons différentes.

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

Solution simple

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

Une fois tous les threads terminés, décidez quoi faire avec les exceptions. Cela signifie que tous les autres threads ont poursuivi leur traitement, ce qui n'est peut-être pas ce que vous voulez.

Solution complexe

La solution la plus complexe consiste à faire vérifier chacun de vos threads aux points stratégiques de leur exécution, si une exception a été levée à partir d'un autre thread.

Si un thread lève une exception, il est intercepté avant de quitter le thread, l'objet d'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 abandonne de manière gracieuse.

Lorsque tous les threads ont été abandonnés, le thread principal peut gérer l'exception selon les besoins.

6
paercebal

Une exception levée à partir d'un thread ne sera pas capturable dans le thread parent. Les threads ont des contextes et des piles différents, et généralement le thread parent n'est pas obligé de rester là et d'attendre la fin des enfants, afin qu'il puisse intercepter 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 à l'intérieur de chaque thread et interpréter l'état de sortie des threads du thread principal pour relancer toutes les exceptions dont vous pourriez avoir besoin.

BTW, en l'absence d'une capture dans un thread, il est spécifique à l'implémentation si le déroulement de la pile sera effectué, c'est-à-dire que les destructeurs de vos variables automatiques peuvent même ne pas être appelés avant l'appel de terminate. Certains compilateurs le font, mais ce n'est pas obligatoire.

4
n-alexander

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

3
tvanfosson

Il n'y a, en effet, aucun moyen bon et générique de transmettre des exceptions d'un thread au suivant.

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

Si toutes vos exceptions n'héritent pas de std :: exception, vous êtes en difficulté et devez écrire beaucoup de captures de niveau supérieur dans votre thread ... mais la solution tient toujours.

2
PierreBdR

Vous devrez effectuer une capture générique pour toutes les exceptions dans le travailleur (y compris les exceptions non std, comme les violations d'accès) et envoyer un message à partir du thread de travail (je suppose que vous avez une sorte de messagerie en place?) Au contrôleur. thread, contenant un pointeur en direct vers l'exception, et y retourner en créant une copie de l'exception. Ensuite, le travailleur peut libérer l'objet d'origine et quitter.

1
anon6439

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 rejoindre un thread enfant, qui relance automatiquement (à l'aide de boost :: rethrow_exception) toute exception émise par un thread enfant.

1
Emil