Wie kann ich Ausnahmen zwischen Threads verbreiten?

105

Wir haben eine Funktion, die ein einzelner Thread aufruft (wir nennen dies den Haupt-Thread). Innerhalb des Funktionskörpers erzeugen wir mehrere Worker-Threads, um CPU-intensive Arbeit zu leisten, warten, bis alle Threads beendet sind, und geben dann das Ergebnis im Haupt-Thread zurück.

Das Ergebnis ist, dass der Aufrufer die Funktion naiv verwenden kann und intern mehrere Kerne verwendet.

Alles gut soweit ..

Das Problem, das wir haben, ist der Umgang mit Ausnahmen. Wir möchten nicht, dass Ausnahmen in den Arbeitsthreads die Anwendung zum Absturz bringen. Wir möchten, dass der Aufrufer der Funktion sie im Hauptthread abfangen kann. Wir müssen Ausnahmen für die Worker-Threads abfangen und sie an den Haupt-Thread weitergeben, damit sie von dort aus weiter abgewickelt werden.

Wie können wir das machen?

Das Beste, was ich mir vorstellen kann, ist:

  1. Fangen Sie eine ganze Reihe von Ausnahmen in unseren Worker-Threads ab (std :: exception und einige unserer eigenen).
  2. Notieren Sie den Typ und die Nachricht der Ausnahme.
  3. Verfügen Sie über eine entsprechende switch-Anweisung im Hauptthread, die Ausnahmen des im Arbeitsthread aufgezeichneten Typs erneut auslöst.

Dies hat den offensichtlichen Nachteil, dass nur eine begrenzte Anzahl von Ausnahmetypen unterstützt wird und Änderungen erforderlich sind, wenn neue Ausnahmetypen hinzugefügt werden.

pauldoo
quelle

Antworten:

89

In C ++ 11 wurde der exception_ptrTyp eingeführt, mit dem Ausnahmen zwischen Threads transportiert werden können:

#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;
}

Da Sie in Ihrem Fall mehrere Worker-Threads haben, müssen Sie exception_ptrfür jeden einen behalten .

Beachten Sie, dass dies exception_ptrein gemeinsam genutzter ptr-ähnlicher Zeiger ist. Sie müssen also mindestens einen Zeiger exception_ptrauf jede Ausnahme behalten, sonst werden sie freigegeben.

Microsoft-spezifisch: Wenn Sie SEH Exceptions ( /EHa) verwenden, transportiert der Beispielcode auch SEH-Ausnahmen wie Zugriffsverletzungen, die möglicherweise nicht Ihren Wünschen entsprechen.

Gerardo Hernandez
quelle
Was ist mit mehreren Threads, die aus main hervorgegangen sind? Wenn der erste Thread eine Ausnahme trifft und beendet wird, wartet main () auf den zweiten Thread join (), der möglicherweise für immer ausgeführt wird. main () würde nach den beiden Joins () niemals teptr testen können. Es scheint, dass alle Threads den globalen Teptr regelmäßig überprüfen und gegebenenfalls beenden müssen. Gibt es einen sauberen Weg, um mit dieser Situation umzugehen?
Cosmo
75

Derzeit besteht die einzige tragbare Möglichkeit darin, Catch-Klauseln für alle Arten von Ausnahmen zu schreiben, die Sie möglicherweise zwischen Threads übertragen möchten, die Informationen irgendwo aus dieser Catch-Klausel zu speichern und sie später zum erneuten Auslösen einer Ausnahme zu verwenden. Dies ist der Ansatz von Boost.Exception .

In C ++ 0x können Sie eine Ausnahme mit abfangen catch(...)und dann in einer std::exception_ptrVerwendungsinstanz speichern std::current_exception(). Sie können es dann später aus demselben oder einem anderen Thread mit erneut werfen std::rethrow_exception().

Wenn Sie Microsoft Visual Studio 2005 oder höher verwenden, wird die Thread-Bibliothek just :: thread C ++ 0x unterstützt std::exception_ptr. (Haftungsausschluss: Dies ist mein Produkt).

Anthony Williams
quelle
7
Dies ist jetzt Teil von C ++ 11 und wird von MSVS 2010 unterstützt. Siehe msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde
7
Es wird auch von gcc 4.4+ unter Linux unterstützt.
Anthony Williams
Cool, es gibt einen Link für ein Anwendungsbeispiel: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke
11

Wenn Sie mit C ++ 11, dann std::futurekönnte genau das tun , was Sie suchen: es kann automatisch Falle Ausnahmen , die es an die Spitze des Arbeitsthread machen, und geben sie nicht an dem Punkt , an dem übergeordneten Faden durch das std::future::getist namens. (Hinter den Kulissen geschieht dies genau wie in der Antwort von @AnthonyWilliams; es wurde bereits für Sie implementiert.)

Der Nachteil ist, dass es keinen Standardweg gibt, sich nicht mehr um a zu kümmern std::future. Sogar sein Destruktor wird einfach blockiert, bis die Aufgabe erledigt ist. [EDIT 2017: Der Sperr destructor Verhalten ist eine misfeature nur des pseudo-Futures wieder aus std::async, die Sie sowieso nie verwenden sollten. Normale Futures blockieren ihren Destruktor nicht. Aber Sie können Aufgaben immer noch nicht "abbrechen", wenn Sie sie verwenden std::future: Die Aufgaben, die Versprechen erfüllen, werden weiterhin hinter den Kulissen ausgeführt, auch wenn niemand mehr auf die Antwort hört.] Hier ist ein Spielzeugbeispiel, das möglicherweise klarstellt, was ich tue bedeuten:

#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());
    }
}

Ich habe gerade versucht, ein arbeitsähnliches Beispiel mit std::threadund zu schreiben std::exception_ptr, aber mit std::exception_ptr(mit libc ++) läuft etwas schief, sodass ich es noch nicht zum Laufen gebracht habe. :((

[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!");
    }
}

Ich habe keine Ahnung, was ich 2013 falsch gemacht habe, aber ich bin mir sicher, dass es meine Schuld war.]

Quuxpluson
quelle
Warum ordnen Sie die erstellte Zukunft einem Namen zu fund dann emplace_back? Könntest du nicht einfach etwas tun waitables.push_back(std::async(…));oder übersehen ich etwas (es kompiliert, die Frage ist, ob es auslaufen könnte, aber ich sehe nicht wie)?
Konrad Rudolph
1
Gibt es auch eine Möglichkeit, den Stapel abzuwickeln, indem die Futures abgebrochen werden, anstatt zu gehen wait? Etwas in der Art, „sobald einer der Jobs fehlgeschlagen ist, spielen die anderen keine Rolle mehr“.
Konrad Rudolph
4 Jahre später ist meine Antwort nicht gut gealtert. :) Zu "Warum": Ich denke, es war nur aus Gründen der Klarheit (um zu zeigen, dass asynceine Zukunft eher zurückkommt als etwas anderes). Zu "Also, is there": Nicht in std::future, aber siehe Sean Parents Vortrag "Better Code: Concurrency" oder meine "Futures from Scratch" für verschiedene Möglichkeiten, dies zu implementieren, wenn es Ihnen nichts ausmacht, die gesamte STL für den Anfang neu zu schreiben. :) Der Schlüsselsuchbegriff lautet "Stornierung".
Quuxplusone
Danke für deine Antwort. Ich werde mir auf jeden Fall die Gespräche ansehen, wenn ich eine Minute finde.
Konrad Rudolph
1
Gute 2017 bearbeiten. Entspricht dem akzeptierten, jedoch mit einem Ausnahmezeiger. Ich würde es oben platzieren und vielleicht sogar den Rest loswerden.
Nathan Cooper
6

Ihr Problem ist, dass Sie möglicherweise mehrere Ausnahmen von mehreren Threads erhalten, da jede fehlschlagen kann, möglicherweise aus verschiedenen Gründen.

Ich gehe davon aus, dass der Haupt-Thread irgendwie darauf wartet, dass die Threads enden, um die Ergebnisse abzurufen, oder regelmäßig den Fortschritt der anderen Threads überprüft und dass der Zugriff auf gemeinsam genutzte Daten synchronisiert wird.

Einfache Lösung

Die einfache Lösung wäre, alle Ausnahmen in jedem Thread abzufangen und in einer gemeinsam genutzten Variablen (im Haupt-Thread) aufzuzeichnen.

Wenn alle Threads abgeschlossen sind, entscheiden Sie, was mit den Ausnahmen geschehen soll. Dies bedeutet, dass alle anderen Threads ihre Verarbeitung fortgesetzt haben, was möglicherweise nicht das ist, was Sie wollen.

Komplexe Lösung

Die komplexere Lösung besteht darin, dass jeder Ihrer Threads an strategischen Punkten seiner Ausführung überprüft, ob eine Ausnahme von einem anderen Thread ausgelöst wurde.

Wenn ein Thread eine Ausnahme auslöst, wird sie vor dem Beenden des Threads abgefangen, das Ausnahmeobjekt wird in einen Container im Hauptthread kopiert (wie in der einfachen Lösung) und eine gemeinsam genutzte boolesche Variable wird auf true gesetzt.

Und wenn ein anderer Thread diesen Booleschen Wert testet, sieht er, dass die Ausführung abgebrochen werden soll, und bricht auf anmutige Weise ab.

Wenn alle Threads abgebrochen wurden, kann der Haupt-Thread die Ausnahme nach Bedarf behandeln.

paercebal
quelle
4

Eine von einem Thread ausgelöste Ausnahme kann im übergeordneten Thread nicht abgefangen werden. Threads haben unterschiedliche Kontexte und Stapel, und im Allgemeinen muss der übergeordnete Thread nicht dort bleiben und warten, bis die untergeordneten Threads fertig sind, damit sie ihre Ausnahmen abfangen können. Es gibt einfach keinen Platz im Code für diesen Fang:

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

Sie müssen Ausnahmen in jedem Thread abfangen und den Beendigungsstatus von Threads im Hauptthread interpretieren, um eventuell benötigte Ausnahmen erneut auszulösen.

Übrigens ist es in Abwesenheit eines Catch in einem Thread implementierungsspezifisch, ob das Abwickeln des Stapels überhaupt durchgeführt wird, dh die Destruktoren Ihrer automatischen Variablen werden möglicherweise nicht einmal aufgerufen, bevor terminate aufgerufen wird. Einige Compiler tun dies, aber es ist nicht erforderlich.

n-Alexander
quelle
3

Könnten Sie die Ausnahme im Worker-Thread serialisieren, an den Haupt-Thread zurücksenden, deserialisieren und erneut auslösen? Ich gehe davon aus, dass die Ausnahmen alle von derselben Klasse stammen müssen (oder zumindest von einer kleinen Menge von Klassen mit der switch-Anweisung). Ich bin mir auch nicht sicher, ob sie serialisierbar sind, ich denke nur laut nach.

Tvanfosson
quelle
Warum muss man es serialisieren, wenn sich beide Threads im selben Prozess befinden?
Nawaz
1
@Nawaz, da die Ausnahme wahrscheinlich Verweise auf threadlokale Variablen enthält, die anderen Threads nicht automatisch zur Verfügung stehen.
Tvanfosson
2

Es gibt in der Tat keine gute und generische Möglichkeit, Ausnahmen von einem Thread zum nächsten zu übertragen.

Wenn alle Ihre Ausnahmen wie gewünscht von std :: exception abgeleitet sind, können Sie einen allgemeinen Ausnahmefang der obersten Ebene haben, der die Ausnahme irgendwie an den Hauptthread sendet, wo sie erneut ausgelöst wird. Das Problem ist, dass Sie den Wurfpunkt der Ausnahme verlieren. Sie können wahrscheinlich compilerabhängigen Code schreiben, um diese Informationen abzurufen und zu übertragen.

Wenn nicht alle Ihre Ausnahmen std :: exception erben, sind Sie in Schwierigkeiten und müssen viel Catch auf oberster Ebene in Ihren Thread schreiben ... aber die Lösung bleibt bestehen.

PierreBdR
quelle
1

Sie müssen einen generischen Fang für alle Ausnahmen im Worker durchführen (einschließlich nicht standardmäßiger Ausnahmen wie Zugriffsverletzungen) und eine Nachricht vom Worker-Thread (ich nehme an, Sie haben eine Art von Messaging eingerichtet?) An das Controlling senden Thread, der einen Live-Zeiger auf die Ausnahme enthält, und erneut werfen, indem eine Kopie der Ausnahme erstellt wird. Dann kann der Arbeiter das ursprüngliche Objekt freigeben und beenden.

anon6439
quelle