Comment puis-je propager des exceptions entre les threads?

105

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 en CPU, attendons que tous les threads se terminent, 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 est celui des exceptions. Nous ne voulons pas que les exceptions sur les threads de travail bloquent l'application. Nous voulons que l'appelant de la fonction puisse les attraper sur le thread principal. Nous devons intercepter les exceptions sur les threads de travail et les propager vers le thread principal pour qu'elles continuent à se dérouler à partir de là.

Comment peut-on le faire?

Le mieux auquel je puisse penser est:

  1. Attrapez toute une variété d'exceptions sur nos threads de travail (std :: exception et quelques-unes 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 de tout 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'exceptions et nécessiterait une modification chaque fois que de nouveaux types d'exceptions seraient ajoutés.

Pauldoo
la source

Réponses:

89

C ++ 11 a introduit le exception_ptrtype 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_ptrpour chacun d'eux.

Notez qu'il exception_ptrs'agit d'un pointeur de type ptr partagé, vous devrez donc en conserver au moins un exception_ptrpointant vers chaque exception ou ils seront libérés.

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

Gerardo Hernandez
la source
Qu'en est-il de plusieurs threads générés hors de main? Si le premier thread rencontre une exception et se termine, main () attendra le second thread join () qui pourrait s'exécuter indéfiniment. main () ne pourrait jamais tester teptr après les deux jointures (). Il semble que tous les threads ont besoin de vérifier périodiquement le teptr global et de quitter le cas échéant. Existe-t-il un moyen propre de gérer cette situation?
Cosmo
75

Actuellement, le seul moyen portable est d'écrire des clauses catch pour tous les types d'exceptions que vous pourriez souhaiter transférer entre les threads, de stocker les informations quelque part à partir de cette clause catch et de les utiliser plus tard pour renvoyer une exception. C'est l'approche adoptée par Boost. .

En C ++ 0x, vous pourrez intercepter une exception avec catch(...), puis la stocker dans une instance d' std::exception_ptrutilisation std::current_exception(). Vous pouvez ensuite le relancer ultérieurement à partir du même fil ou d'un autre fil 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. (Avertissement: c'est mon produit).

Anthony Williams
la source
7
Cela fait maintenant partie de C ++ 11 et est pris en charge par MSVS 2010; voir msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde
7
Il est également pris en charge par gcc 4.4+ sur Linux.
Anthony Williams
Cool, il y a un lien pour un exemple d'utilisation: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke
11

Si vous utilisez C ++ 11, vous std::futurepouvez alors faire exactement ce que vous recherchez: il peut intercepter automatiquement 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; cela vient d'être implémenté pour vous déjà.)

L'inconvénient est qu'il n'y a pas de méthode standard pour "arrêter de se soucier de" a std::future; même son destructeur bloquera simplement jusqu'à ce que la tâche soit terminée. [EDIT, 2017: Le comportement bloquant-destructeur est un dysfonctionnement uniquement des pseudo-futurs retournés 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 de réalisation de promesses continueront à s'exécuter dans les coulisses même si personne n'écoute plus la réponse.] Voici un exemple de jouet qui pourrait clarifier ce que je signifier:

#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 en utilisant std::threadet std::exception_ptr, mais quelque chose ne va pas avecstd::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 j'ai fait de mal en 2013, mais je suis sûr que c'était de ma faute.]

Quuxplusone
la source
Pourquoi attribuez-vous le futur de création à un nommé fet ensuite emplace_back? Ne pourrais-tu pas juste faire waitables.push_back(std::async(…));ou est-ce que je néglige quelque chose (il compile, la question est de savoir si cela pourrait fuir, mais je ne vois pas comment)?
Konrad Rudolph
1
En outre, existe-t-il un moyen de dérouler la pile en annulant les futures au lieu de les waitingérer? Quelque chose du genre «dès qu'un des travaux a échoué, les autres n'ont plus d'importance».
Konrad Rudolph
4 ans plus tard, ma réponse n'a pas bien vieilli. :) Re "Why": Je pense que c'était juste pour plus de clarté (pour montrer que cela asyncrenvoie un futur plutôt que quelque chose d'autre). Re "Aussi, y a-t-il": Pas dedans std::future, mais voir le discours de Sean Parent "Better Code: Concurrency" ou mon "Futures from Scratch" pour différentes façons de l'implémenter si cela ne vous dérange pas de réécrire la totalité de la STL pour commencer. :) Le terme de recherche clé est «annulation».
Quuxplusone
Merci pour votre réponse. Je vais certainement jeter un œil aux discussions quand j'en trouverai une minute.
Konrad Rudolph
1
Bonne édition 2017. Identique à l'accepté, mais avec un pointeur d'exception à portée. Je le mettrais au sommet et peut-être même me débarrasserais du reste.
Nathan Cooper
6

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 d'une manière ou d'une autre 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 de capturer 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 continué leur traitement, ce qui n'est peut-être pas ce que vous souhaitez.

Solution complexe

La solution la plus complexe consiste à faire vérifier chacun de vos threads à des points stratégiques de leur exécution, si une exception a été levée depuis 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 quand un autre thread teste ce booléen, il voit que l'exécution doit être abandonnée et s'interrompt de manière gracieuse.

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

Paercebal
la source
4

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 que les enfants se terminent, afin de pouvoir 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 dans chaque thread et interpréter l'état de sortie des threads du thread principal pour relancer les exceptions dont vous pourriez avoir besoin.

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

n-alexander
la source
3

Pourriez-vous sérialiser l'exception dans le thread de travail, la transmettre 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 du moins d'un petit ensemble de classes avec à nouveau l'instruction switch). De plus, je ne suis pas sûr qu'ils seraient sérialisables, je pense juste à voix haute.

Tvanfosson
la source
Pourquoi faut-il le sérialiser si les deux threads sont dans le même processus?
Nawaz
1
@Nawaz car l'exception a probablement des références à des variables locales de thread qui ne sont pas automatiquement disponibles pour d'autres threads.
tvanfosson
2

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

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 d'une manière ou d'une autre l'exception au thread principal où elle sera à nouveau lancée. Le problème est que vous perdez le point de départ 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, alors vous avez des problèmes et devez écrire beaucoup de captures de niveau supérieur dans votre thread ... mais la solution tient toujours.

PierreBdR
la source
1

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

anon6439
la source