CodeGym /Kurse /C++ SELF /Ausnahmen im async‑Code

Ausnahmen im async‑Code

C++ SELF
Level 71, Lektion 3
Verfügbar

1. Warum Ausnahmen in Multithreading „nicht dort auftauchen“

Wenn Sie gerade erst anfangen, nebenläufigen Code zu schreiben, ist die Erwartung normalerweise: „Wenn dort ein Fehler passiert, fange ich ihn in der Nähe, wo die Aufgabe gestartet wurde“. Aber Nebenläufigkeit hat ihren eigenen Humor. Eine Ausnahme tritt in einem Ausführungs‑Kontext auf, und wir sehen sie oft in einem anderen. Das bricht das gewohnte Weltbild, in dem throw und catch nahe beieinanderstehen und Sie nicht auf eine Spurensuche schicken.

Bei klassischem std::thread ist die Situation noch härter: wenn eine Ausnahme aus der Thread‑Funktion ungefangen herausfliegt, beendet das Programm gewöhnlich die Ausführung via std::terminate() (also ein „Not‑Abbruch ohne Diskussion“). std::async/std::future sind humaner: sie bieten einen „Kanal“, über den die Ausnahme bis zu der Stelle gelangt, an der Sie das Ergebnis abholen.

Wie future Wert oder Ausnahme speichert

Wenn wir sagen „std::future speichert das Ergebnis“, ist es hilfreich, präzise zu sein: future speichert nicht das Ergebnis selbst, sondern eine Referenz auf den gemeinsamen Zustand (shared state). Das ist eine interne „Zelle“, in der schließlich genau eine von zwei Möglichkeiten liegt: entweder ein Wert vom Typ T oder eine Ausnahme. Wichtig ist, dass get() diesen Zustand entnimmt — der Standard behandelt sogar explizit das Verhalten beim „Freigeben des shared state“ durch future::get().

Vereinfacht dargestellt:

flowchart LR
    A["Task start (async / promise / packaged_task)"] --> B["Shared state"]
    B --> C["value: T"]
    B --> D["exception: exception_ptr"]
    E["Consumer (future)"] -->|wait / get| B

Aus dieser Darstellung folgt der wichtigste praktische Schluss der Vorlesung: die Ausnahme wird sichtbar nicht dort, wo sie entstanden ist, sondern dort, wo Sie future.get() aufrufen.

2. Wo Fehler bei std::async abfangen

Wenn Sie std::async verwenden, treten zeitlich zwei Arten von Fehlern auf. Das ist wichtig, weil unterschiedliche Fehler an unterschiedlichen Stellen gefangen werden (und genau hier haben Anfänger meist die größten Probleme).

Fehler beim Aufruf von std::async

Zuerst eine kurze Vorbemerkung. std::async ist kein magischer „mach es parallel“-Knopf, sondern eine normale Funktion, die die Aufgabe vorbereiten muss: Argumente kopieren/verschieben, Callable verpacken, manchmal einen Thread anlegen (abhängig von der Start‑Policy). Und bei diesem Schritt kann etwas schiefgehen.

Zum Beispiel kann theoretisch das Erstellen eines Threads fehlschlagen (Systemfehler), oder das Kopieren von Argumenten kann eine Ausnahme werfen (selten, aber möglich). Solche Fehler treten sofort auf, genau in der Zeile std::async(...), und werden mit einem normalen try/catch um den Start herum gefangen.

Kurzbeispiel (demonstrativ; die reale Ursache hängt von der Situation ab):

#include <future>
#include <iostream>
#include <stdexcept>

int main() {
    try {
        auto f = std::async(std::launch::async, [] {
            return 1;
        });
        std::cout << f.get() << '\n'; // 1
    } catch (const std::exception& e) {
        std::cout << "launch failed: " << e.what() << '\n';
    }
}

Fehler während der Ausführung der Aufgabe

Jetzt der wichtige Teil. Wenn das Callable innerhalb von async eine Ausnahme wirft, wird diese nicht automatisch ausgegeben und trifft nicht sofort bei Ihnen ein. Sie wird im shared state abgelegt (als exception_ptr) und beim Aufruf von future.get() erneut geworfen.

#include <future>
#include <iostream>
#include <stdexcept>

int main() {
    auto f = std::async(std::launch::async, []() -> int {
        throw std::runtime_error("boom in async");
    });

    try {
        std::cout << f.get() << '\n'; // wir kommen hier nicht an
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << '\n'; // caught: boom in async
    }
}

Beachten Sie die „psychologische Falle“: der Fehler ist „dort drüben“ passiert, und gefangen wird er „genau hier“.

4. Warum wait() und wait_for() keinen Erfolg garantieren

Nachdem Sie gesehen haben, dass die Ausnahme bei get() auftritt, denkt man oft: „Ok, dann rufe ich zuerst wait() auf, stelle sicher, dass es fertig ist, und dann klappt alles“. Leider bedeutet „fertig“ nicht „erfolgreich“. Es heißt nur, dass im shared state bereits ein Ergebnis liegt — und dieses Ergebnis kann eine Ausnahme sein.

Demonstration:

#include <future>
#include <iostream>
#include <stdexcept>

int main() {
    auto f = std::async(std::launch::async, []() -> int {
        throw std::runtime_error("fail");
    });

    f.wait(); // wir haben gewartet, kennen aber das Ergebnis nicht

    try {
        (void)f.get(); // die Ausnahme tritt genau hier zutage
    } catch (const std::exception& e) {
        std::cout << "after wait: " << e.what() << '\n'; // after wait: fail
    }
}

Bei wait_for() und Timeouts ist das gleiche Spiel. Ein Timeout bedeutet nicht „Fehler der Aufgabe“, sondern schlicht „nicht rechtzeitig fertig geworden“. Und selbst wenn es fertig geworden ist — das garantiert trotzdem keinen Erfolg, denn der Erfolg zeigt sich erst bei get().

5. Modus std::launch::deferred: die Ausnahme wirkt synchron

std::launch::deferred ist ein Modus, bei dem die Aufgabe nicht sofort gestartet wird, sondern erst beim Aufruf von wait()/get() im wartenden Thread ausgeführt wird. Und hier denkt man sofort: „Dann passiert die Ausnahme genau hier“ — und das stimmt. Aber die Regel bleibt: die Beobachtung erfolgt bei get(); nur dass nun Ausführungsort und Beobachtungsort zusammenfallen.

#include <future>
#include <iostream>
#include <stdexcept>

int main() {
    auto f = std::async(std::launch::deferred, []() -> int {
        throw std::runtime_error("deferred fail");
    });

    try {
        std::cout << f.get() << '\n';
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << '\n'; // caught: deferred fail
    }
}

Praktische Konsequenz: wenn Sie deferred sehen, erwarten Sie keine Parallelität und bedenken Sie, dass es möglicherweise keinen Background‑Thread gab.

6. Fehlerübergabe über std::promise: set_exception und exception_ptr

Manchmal passt std::async nicht, weil das Ergebnis nicht per return geliefert wird, sondern als Ereignis: „Antwort eingetroffen“, „Verarbeitung beendet“, „Fehler aufgetreten“. Dann arbeiten wir mit std::promise<T> und entscheiden selbst, was in den shared state gelegt wird: ein Wert oder eine Ausnahme.

Dafür verwendet man std::promise<T>::set_exception(...), das ein std::exception_ptr entgegennimmt. Ein exception_ptr lässt sich auf zwei grundlegende Arten erzeugen.

std::make_exception_ptr(...): Ausnahme explizit erstellen

Kurz erklärt: Manchmal fängt man keine reale Ausnahme, sondern möchte „signalieren“, dass ein Fehler vorliegt (z. B. ungültige Eingabedaten, Datei nicht gefunden, falsches Format). Dann ist es praktisch, eine Ausnahme explizit zu erzeugen und zu verpacken.

#include <exception>
#include <future>
#include <iostream>
#include <stdexcept>

int main() {
    std::promise<int> p;
    auto f = p.get_future();

    p.set_exception(std::make_exception_ptr(std::runtime_error("bad input")));

    try {
        (void)f.get();
    } catch (const std::exception& e) {
        std::cout << e.what() << '\n'; // bad input
    }
}

std::current_exception(): gefangene Ausnahme weiterleiten

Wenn Sie sich bereits in einem catch (...) befinden, liefert std::current_exception() ein exception_ptr auf die aktuell gefangene Ausnahme. Das ist das Standardmuster, um eine Ausnahme aus Worker‑Code an den Consumer weiterzureichen.

#include <exception>
#include <future>
#include <stdexcept>

void worker(std::promise<int> p) {
    try {
        throw std::runtime_error("worker failed");
    } catch (...) {
        p.set_exception(std::current_exception());
    }
}

Die Tatsache, dass exception_ptr als Mechanismus zur Übertragung von Ausnahmen zwischen Ausführungskontexten existiert und aktiv weiterentwickelt wird, wird in den Materialien des Komitees regelmäßig diskutiert — es ist also kein „Hack“, sondern ein normaler Teil des Exception‑Modells in modernem C++.

7. Praktisches Beispiel: TaskBook‑Report und Fehlerbehandlung

Wir verbinden das mit der „einheitlichen Kursanwendung“. Angenommen, wir haben eine Konsolenanwendung TaskBook: sie speichert eine Liste von Tasks (Titel + geschätzte Zeit), kann sie ausgeben und speichern/laden (egal wie — bis zu diesem Kurspunkt haben Sie Dateien, JSON und CLI gesehen).

Nun fügen wir ein Feature hinzu: „Report über Tasks generieren“. Der Report kann aufwändig sein (Analyse, Sortierung, Aggregationen) — und wir wollen ihn asynchron über std::async erstellen. Außerdem möchten wir, dass der Anwender bei Fehlschlag eine verständliche Meldung sieht und das Programm nicht still stirbt.

Datenmodell

#include <string>

struct Task {
    std::string title;
    int minutes = 0;
};

Report‑Erstellungsfunktion

#include <numeric>
#include <stdexcept>
#include <string>
#include <vector>

std::string buildReport(const std::vector<Task>& tasks) {
    if (tasks.empty()) {
        throw std::runtime_error("no tasks to report");
    }

    int total = 0;
    for (const Task& t : tasks) total += t.minutes;

    return "Tasks: " + std::to_string(tasks.size()) +
           ", total minutes: " + std::to_string(total);
}

Hier werfen wir absichtlich eine Ausnahme bei einer leeren Liste. Das ist ein Lehrbeispiel: in der Praxis könnten Sie einen leeren Report zurückgeben, aber hier geht es darum, die Liefermechanik der Fehler zu sehen.

Asynchroner Start und die richtige Stelle für try/catch

#include <future>
#include <iostream>
#include <vector>

int main() {
    std::vector<Task> tasks; // angenommen, noch leer

    auto reportFuture = std::async(std::launch::async, buildReport, tasks);

    try {
        std::string report = reportFuture.get();
        std::cout << report << '\n';
    } catch (const std::exception& e) {
        std::cout << "Cannot build report: " << e.what() << '\n';
        // Cannot build report: no tasks to report
    }
}

Wichtig: fangen Sie um get(). Wenn Sie try/catch um std::async legen, fangen Sie nur „Start‑Fehler“, nicht „Ausführungs‑Fehler“.

8. Nützliche Feinheiten zu future: broken promise und Vertragsfehler

Dieser Abschnitt behandelt Situationen, die oft wie „Task abgestürzt“ aussehen, obwohl es in Wirklichkeit ein Vertragsbruch zwischen promise und future oder eine nicht offensichtliche API‑Eigenschaft ist.

Broken promise: „Versprochenes Ergebnis ist verschwunden“

Broken promise ist die Situation, in der ein std::promise zerstört wird, ohne jemals set_value oder set_exception aufzurufen. Für den Consumer (future) sieht das wie ein Vertragsbruch aus: „Ich habe auf ein Ergebnis gewartet, aber der Anbieter ist weg“. In der Folge wirft future.get() eine Ausnahme vom Typ std::future_error.

Für Anfänger ist das eine hilfreiche Diagnose, weil sie schnell zeigt: „Sie haben vergessen, den Ergebnis‑Kanal abzuschließen“.

Kurzdemo:

#include <future>
#include <iostream>

int main() {
    std::future<int> f;

    {
        std::promise<int> p;
        f = p.get_future();
        // p wird zerstört, aber set_value/set_exception wird nicht aufgerufen
    }

    try {
        (void)f.get(); // wirft std::future_error (broken promise)
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << '\n';
    }
}

Im realen Code bedeutet broken promise oft eines von dreien: der Thread hat früher beendet, eine „frühe Rückkehr“-Logik vergisst die Fehlermeldung, oder das promise wurde an die falsche Stelle verschoben und „ging verloren“.

get() ist einmalig: ein zweiter get() ist kein Rechenfehler

Eine kleine, aber wichtige Anmerkung. Wenn Sie get() aufgerufen haben, haben Sie das Ergebnis und den shared state konsumiert (der Standard betont das Freigeben des shared state durch get()). Ein zweiter Aufruf von get() verletzt den Nutzungsvertrag und äußert sich normalerweise durch eine std::future_error.

#include <future>
#include <iostream>

int main() {
    auto f = std::async(std::launch::async, [] { return 5; });

    std::cout << f.get() << '\n'; // 5

    try {
        std::cout << f.get() << '\n'; // Fehler: zweiter get()
    } catch (const std::exception& e) {
        std::cout << "second get: " << e.what() << '\n';
    }
}

Praktische Regel: wenn ein Wert mehrfach gebraucht wird — speichern Sie ihn in einer Variable (oder verwenden Sie andere Mechanismen; darauf gehen wir hier nicht ein).

wait_for() kann ready zurückgeben, und get() wirft trotzdem

Das ist eine logische Fortsetzung des Themas. ready heißt „das Ergebnis ist bereit“. Das Ergebnis kann aber eine Ausnahme sein.

#include <chrono>
#include <future>
#include <iostream>
#include <stdexcept>

int main() {
    auto f = std::async(std::launch::async, []() -> int {
        throw std::runtime_error("fail after ready");
    });

    auto st = f.wait_for(std::chrono::seconds{1});
    if (st == std::future_status::ready) {
        std::cout << "ready\n"; // ready
    }

    try {
        (void)f.get();
    } catch (const std::exception& e) {
        std::cout << e.what() << '\n'; // fail after ready
    }
}

Disziplinmuster: shared state immer abschließen

Formulieren wir die Disziplin klar, die Nerven spart. Wenn Sie einen Producer/Consumer über promise/future haben, sollte der Producer die eiserne Gewohnheit haben: jeder Weg aus dem Producer‑Code muss zu set_value oder set_exception führen.

Ein sicheres Muster (etwas mehr Code, aber lohnenswert):

#include <exception>
#include <future>
#include <stdexcept>

void produceReport(std::promise<std::string> p) {
    try {
        // ... Arbeit erledigen
        throw std::runtime_error("report failed");
        // p.set_value("ok"); // im \"guten\" Fall stünde das hier
    } catch (...) {
        p.set_exception(std::current_exception());
    }
}

Der Punkt ist: der Consumer kann immer get() aufrufen und entweder einen Wert oder eine normale Ausnahme erhalten — nicht „Stille“ und einen broken promise.

9. Typische Fehler

Fehler Nr. 1: try/catch um std::async statt um future.get().
Solcher Code fängt nur „Start‑Fehler“, nicht die Fehler, die während der Ausführung des Callable passieren. Deshalb sieht das Programm manchmal so aus, als würde es „ohne Grund abstürzen“. Die Heilung ist einfach: die Beobachtung des Ergebnisses ist get(), also setzen Sie dort das try/catch.

Fehler Nr. 2: denken, dass wait() Erfolg bedeutet.
wait() und wait_for() beantworten die Frage „ist der gemeinsame Zustand bereit“, nicht „war die Berechnung erfolgreich“. Daher rufen Anfänger wait() auf und dann ohne try/catch get() — und wundern sich, dass die Ausnahme trotzdem auftritt. Das richtige mentale Modell: wait — über Zeit, get — über Ergebnis.

Fehler Nr. 3: vergessen, get() aufzurufen und damit die Ausnahme zu „verlieren“.
future ist der Vertrag, das Ergebnis zu bekommen. Wenn Sie eine Aufgabe starten, aber nirgends get() aufrufen (oder das Ergebnis anders behandeln), werfen Sie die Resultate faktisch in ein schwarzes Loch. In Lehrbeispielen ist das harmlos, in echten Systemen verwandelt sich das in „im Hintergrund bricht manchmal etwas, aber niemand weiß was“.

Fehler Nr. 4: promise zerstören, ohne Wert oder Ausnahme zu setzen (broken promise).
Das ist Klassiker: frühe Rückkehr, eine Ausnahme im Producer‑Code, vergessenes set_exception — und der Consumer sieht std::future_error. Das behebt man mit der Disziplin „catch (...) → set_exception(current_exception())“ und der Regel „jeder Pfad schließt den shared state ab“.

Fehler Nr. 5: Ausnahme der „Aufgabe“ und Ausnahme des „Future‑Vertrags“ verwechseln.
Eine Ausnahme aus buildReport() ist ein Fehler der Geschäftslogik. Ein std::future_error beim zweiten get() ist ein API‑Fehler in der Nutzung. Wenn man diese beiden Problemklassen nicht trennt, versucht man womöglich, die Berechnung an der Stelle zu reparieren, an der eigentlich der Lebenszyklus des future behandelt werden sollte (z. B. Ergebnis zwischenspeichern).

1
Aufgabe
C++ SELF, Level 71, Lektion 3
Gesperrt
Bombe im Hintergrund
Bombe im Hintergrund
1
Aufgabe
C++ SELF, Level 71, Lektion 3
Gesperrt
Warten ist kein Erfolg
Warten ist kein Erfolg
1
Aufgabe
C++ SELF, Level 71, Lektion 3
Gesperrt
Wiederholte Ausgabe
Wiederholte Ausgabe
1
Aufgabe
C++ SELF, Level 71, Lektion 3
Gesperrt
Mittelwert bei leerer Liste
Mittelwert bei leerer Liste
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION