it-swarm.com.de

Wie kann ich Ausnahmen zwischen Threads verbreiten?

Wir haben eine Funktion, in die ein einzelner Thread ruft (wir nennen dies den Hauptthread). Innerhalb des Funktionskörpers erzeugen wir mehrere Worker-Threads, um die CPU-intensive Arbeit zu verrichten, 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 so weit ..

Das Problem, das wir haben, sind Ausnahmen. Wir möchten nicht, dass Ausnahmen in den Worker-Threads die Anwendung zum Absturz bringen. Wir möchten, dass der Aufrufer der Funktion sie im Haupt-Thread abfängt. Wir müssen Ausnahmen auf den Worker-Threads abfangen und sie auf den Haupt-Thread übertragen, damit sie von dort weiter abgewickelt werden.

Wie können wir das machen?

Das Beste, woran ich denken 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 Meldung der Ausnahme.
  3. Verfügen Sie über eine entsprechende switch-Anweisung im Hauptthread, mit der Ausnahmen aller Art, die im Arbeitsthread aufgezeichnet wurden, erneut ausgelöst werden.

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

102
pauldoo

C++ 11 führte das exception_ptr Typ, 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 Arbeitsthreads haben, müssen Sie einen exception_ptr für jeden von ihnen.

Beachten Sie, dass exception_ptr ist ein gemeinsamer ptr-ähnlicher Zeiger, daher müssen Sie mindestens einen exception_ptr auf jede Ausnahme zeigen oder sie werden freigegeben.

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

73

Gegenwärtig besteht die einzige portable Möglichkeit darin, Catch-Klauseln für alle Arten von Ausnahmen zu schreiben, die Sie möglicherweise zwischen Threads übertragen möchten, und die Informationen irgendwo davon zu speichern catch-Klausel und verwenden Sie sie später, um eine Ausnahme erneut auszulösen. Dies ist der Ansatz von Boost.Exception .

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

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

73

Wenn Sie C++ 11 verwenden, kann std::future Genau das tun, wonach Sie suchen: Es kann automatisch Ausnahmen abfangen, die es an den Anfang des Arbeitsthreads schaffen, und sie an das übergeordnete Element weiterleiten Thread an der Stelle, an der std::future::get aufgerufen wird. (Hinter den Kulissen geschieht dies genauso wie in der Antwort von @AnthonyWilliams; es wurde gerade erst für Sie implementiert.)

Der Nachteil ist, dass es keine Standardmethode gibt, mit der man aufhören kann, sich um einen std::future Zu kümmern. Sogar sein Destruktor wird einfach blocken, bis die Aufgabe erledigt ist. [EDIT, 2017: Das Blocking-Destructor-Verhalten ist ein Fehler nur der von std::async Zurückgegebenen Pseudo-Futures.] Das solltest du sowieso nie benutzen. Normale Futures blockieren ihren Destruktor nicht. Aber du kannst immer noch keine Aufgaben "stornieren", wenn du std::future verwendest: Die versprechenserfüllenden Aufgaben werden weiter ausgeführt hinter den Kulissen, auch wenn niemand mehr auf die Antwort wartet.] Hier ist ein Spielzeugbeispiel, das verdeutlichen könnte, was ich meine:

#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, mit std::thread Und std::exception_ptr Ein Beispiel zu schreiben, aber mit std::exception_ptr (Unter Verwendung von libc ++) läuft etwas schief, sodass ich es nicht wirklich zum Laufen gebracht habe noch. :(

[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 sicher, es war meine Schuld.]

10
Quuxplusone

Ihr Problem ist, dass Sie mehrere Ausnahmen von mehreren Threads erhalten können, da jede aus unterschiedlichen Gründen fehlschlagen kann.

Ich gehe davon aus, dass der Haupt-Thread irgendwie darauf wartet, dass der Thread endet, 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 sie 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. Dies ist möglicherweise nicht das, was Sie möchten.

Komplexe Lösung

Die komplexere Lösung besteht darin, jeden Ihrer Threads an strategischen Punkten ihrer Ausführung überprüfen zu lassen, wenn eine Ausnahme von einem anderen Thread ausgelöst wurde.

Wenn ein Thread eine Ausnahme auslöst, wird sie abgefangen, bevor der Thread beendet wird, 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 muss, und bricht ordnungsgemäß ab.

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

6
paercebal

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 er ihre Ausnahmen abfängt. 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 Haupt-Thread interpretieren, um eventuell benötigte Ausnahmen erneut auszulösen.

Übrigens ist es in Abwesenheit eines Catch in einem Thread implementierungsspezifisch, ob überhaupt ein Stack-Unwinding durchgeführt wird, d. H. Die Destruktoren Ihrer automatischen Variablen werden möglicherweise nicht einmal aufgerufen, bevor terminate aufgerufen wird. Einige Compiler machen das, aber es ist nicht erforderlich.

4
n-alexander

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

3
tvanfosson

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 es sein sollte, von std :: exception stammen, können Sie einen allgemeinen Ausnahmefang auf oberster Ebene haben, der die Ausnahme irgendwie an den Haupt-Thread sendet, wo sie erneut ausgelöst wird. Das Problem ist, dass Sie den Auslöser 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, dann sind Sie in Schwierigkeiten und müssen eine Menge Top-Level-Catch in Ihren Thread schreiben ... aber die Lösung bleibt bestehen.

2
PierreBdR

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

1
anon6439

Siehe http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html . Es ist auch möglich, eine Wrapper-Funktion für jede von Ihnen aufgerufene Funktion zu schreiben, um einem untergeordneten Thread beizutreten, die automatisch alle von einem untergeordneten Thread ausgegebenen Ausnahmen erneut auslöst (mithilfe von boost :: rethrow_exception).

1
Emil