Jak mogę propagować wyjątki między wątkami?


105

Mamy funkcję, do której wywołuje pojedynczy wątek (nazywamy ją głównym wątkiem). W treści funkcji tworzymy wiele wątków roboczych w celu wykonania intensywnej pracy procesora, czekamy na zakończenie wszystkich wątków, a następnie zwracamy wynik w wątku głównym.

W rezultacie wywołujący może korzystać z funkcji naiwnie, a wewnętrznie będzie korzystać z wielu rdzeni.

Jak dotąd wszystko dobrze ..

Problem, jaki mamy, dotyczy wyjątków. Nie chcemy, aby wyjątki w wątkach roboczych powodowały awarię aplikacji. Chcemy, aby obiekt wywołujący funkcję był w stanie przechwycić je w głównym wątku. Musimy przechwytywać wyjątki w wątkach roboczych i przenosić je do głównego wątku, aby mogły się dalej rozwijać.

Jak możemy to zrobić?

Najlepsze, co przychodzi mi do głowy, to:

  1. Wyłap całą różnorodność wyjątków w naszych wątkach roboczych (std :: wyjątek i kilka naszych własnych).
  2. Zapisz typ i treść wyjątku.
  3. Miej odpowiednią instrukcję switch w głównym wątku, która ponownie zgłasza wyjątki dowolnego typu, które zostały zarejestrowane w wątku roboczym.

Ma to oczywistą wadę, ponieważ obsługuje tylko ograniczony zestaw typów wyjątków i wymagałoby modyfikacji po dodaniu nowych typów wyjątków.

Odpowiedzi:


89

C ++ 11 wprowadził exception_ptrtyp umożliwiający transport wyjątków pomiędzy wątkami:

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

Ponieważ w twoim przypadku masz wiele wątków roboczych, będziesz musiał zachować po jednym exception_ptrdla każdego z nich.

Zauważ, że exception_ptrjest to wspólny wskaźnik podobny do ptr, więc będziesz musiał zachować co najmniej jeden exception_ptrwskazujący na każdy wyjątek, inaczej zostaną one zwolnione.

Specyficzne dla firmy Microsoft: jeśli używasz SEH Exceptions ( /EHa), przykładowy kod będzie również transportował wyjątki SEH, takie jak naruszenia dostępu, które mogą nie być tym, czego chcesz.


A co z wieloma wątkami wyłonionymi z main? Jeśli pierwszy wątek trafi w wyjątek i zakończy działanie, main () będzie czekał na drugi wątek join (), który może trwać wiecznie. main () nigdy nie przetestowałoby teptr po dwóch złączeniach (). Wygląda na to, że wszystkie wątki muszą okresowo sprawdzać globalny teptr i kończyć, jeśli to konieczne. Czy istnieje czysty sposób radzenia sobie z tą sytuacją?
Cosmo,

75

Obecnie jedynym przenośnym sposobem jest zapisanie klauzul catch dla wszystkich typów wyjątków, które możesz chcieć przenieść między wątkami, przechowywanie informacji gdzieś z tej klauzuli catch, a następnie użycie ich później do ponownego wyrzucenia wyjątku. Takie jest podejście przyjęte przez Boost.Exception .

W C ++ 0x będziesz mógł złapać wyjątek za pomocą, catch(...)a następnie zapisać go w instancji std::exception_ptrusing std::current_exception(). Następnie możesz ponownie wrzucić go później z tego samego lub innego wątku za pomocą std::rethrow_exception().

Jeśli używasz programu Microsoft Visual Studio 2005 lub nowszego, obsługuje bibliotekę wątków just :: thread C ++ 0xstd::exception_ptr . (Zastrzeżenie: to jest mój produkt).


7
Jest to teraz część C ++ 11 i jest obsługiwane przez MSVS 2010; zobacz msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde,

7
Obsługiwany jest również przez gcc 4.4+ na Linuksie.
Anthony Williams,

Fajnie, jest link do przykładu użycia: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke

11

Jeśli używasz C ++ 11, std::futuremożesz zrobić dokładnie to, czego szukasz: może automagicznie przechwytywać wyjątki, które trafiają na początek wątku roboczego i przekazywać je do wątku nadrzędnego w punkcie, który std::future::getjest nazywa. (Za kulisami dzieje się to dokładnie tak, jak w odpowiedzi @AnthonyWilliams; zostało już zaimplementowane.)

Wadą jest to, że nie ma standardowego sposobu, aby „przestać przejmować się” a std::future; nawet jego destruktor będzie po prostu blokował, dopóki zadanie nie zostanie wykonane. [EDYCJA, 2017: Zachowanie blokująco-niszczycielskie jest nieprawidłowością tylko w zwróconych pseudo-futures, z std::asyncktórych i tak nigdy nie powinieneś używać. Normalne futures nie blokują się w ich destruktorze. Ale nadal nie możesz „anulować” zadań, jeśli używasz std::future: zadania spełniające obietnice będą nadal działały za kulisami, nawet jeśli nikt już nie słucha odpowiedzi.] Oto przykład zabawki, który może wyjaśnić, co ja oznaczać:

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

Właśnie próbowałem napisać podobny do pracy przykład przy użyciu std::threadi std::exception_ptr, ale coś jest nie tak z std::exception_ptr(używając libc ++), więc nie udało mi się jeszcze tego zrobić. :(

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

Nie mam pojęcia, co robiłem źle w 2013 roku, ale jestem pewien, że to moja wina.]


Dlaczego przypisujesz przyszłość kreacji do nazwanego, fa potem emplace_backto? Nie mógłbyś po prostu zrobić, waitables.push_back(std::async(…));czy coś przeoczę (kompiluje się, pytanie brzmi, czy może wyciekać, ale nie wiem jak)?
Konrad Rudolph

1
Ponadto, czy istnieje sposób na rozwinięcie stosu przez przerwanie kontraktów futures zamiast waitzerwania? Coś w rodzaju „gdy tylko jedno z zadań zawodzi, inne nie mają już znaczenia”.
Konrad Rudolph

4 lata później moja odpowiedź nie postarzała się dobrze. :) Odp. „Dlaczego”: Myślę, że to tylko dla jasności (żeby pokazać, że asynczwraca się raczej przyszłość niż coś innego). W odniesieniu do „Również, czy tam jest”: nie ma std::future, ale zobacz wykład Seana Parenta „Lepszy kod: współbieżność” lub moje „Futures from Scratch”, aby poznać różne sposoby implementacji tego, jeśli nie masz nic przeciwko przepisaniu całego STL na początek. :) Kluczowym terminem wyszukiwania jest „anulowanie”.
Quuxplusone

Dzięki za odpowiedź. Na pewno przyjrzę się rozmowom, kiedy znajdę chwilę.
Konrad Rudolph

1
Dobra edycja 2017. To samo, co akceptowane, ale ze wskaźnikiem wyjątku o określonym zakresie. Postawiłbym go na górze, a może nawet pozbyłbym się reszty.
Nathan Cooper

6

Problem polega na tym, że możesz otrzymać wiele wyjątków z wielu wątków, ponieważ każdy może się nie powieść, być może z różnych powodów.

Zakładam, że główny wątek w jakiś sposób czeka na zakończenie wątków, aby pobrać wyniki lub regularnie sprawdza postęp innych wątków, a dostęp do udostępnionych danych jest zsynchronizowany.

Proste rozwiązanie

Prostym rozwiązaniem byłoby wyłapanie wszystkich wyjątków w każdym wątku i zapisanie ich we wspólnej zmiennej (w głównym wątku).

Po zakończeniu wszystkich wątków zdecyduj, co zrobić z wyjątkami. Oznacza to, że wszystkie inne wątki kontynuowały przetwarzanie, co być może nie jest tym, czego chcesz.

Kompleksowe rozwiązanie

Bardziej złożonym rozwiązaniem jest sprawdzenie każdego wątku w strategicznych punktach ich wykonania, jeśli wyjątek został wyrzucony z innego wątku.

Jeśli wątek zgłasza wyjątek, jest przechwytywany przed wyjściem z wątku, obiekt wyjątku jest kopiowany do jakiegoś kontenera w głównym wątku (jak w prostym rozwiązaniu), a część współdzielonej zmiennej boolowskiej jest ustawiana na wartość true.

A kiedy inny wątek testuje tę wartość logiczną, widzi, że wykonanie ma zostać przerwane i przerywa wykonywanie w wdzięczny sposób.

Po przerwaniu wszystkich wątków główny wątek może obsłużyć wyjątek w razie potrzeby.


4

Wyjątek zgłoszony z wątku nie będzie możliwy do przechwycenia w wątku nadrzędnym. Wątki mają różne konteksty i stosy, a generalnie wątek nadrzędny nie musi tam pozostać i czekać na zakończenie działania elementów podrzędnych, aby mógł przechwycić ich wyjątki. Po prostu nie ma miejsca w kodzie na ten haczyk:

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

Będziesz musiał przechwycić wyjątki w każdym wątku i zinterpretować stan wyjścia z wątków w głównym wątku, aby ponownie wyrzucić wszystkie wyjątki, których możesz potrzebować.

Przy okazji, w przypadku braku catch w wątku jest to specyficzne dla implementacji, czy rozwijanie stosu będzie w ogóle wykonywane, tj. Destruktory zmiennych automatycznych mogą nie zostać wywołane nawet przed wywołaniem terminate. Niektóre kompilatory to robią, ale nie jest to wymagane.


3

Czy możesz serializować wyjątek w wątku roboczym, przesłać go z powrotem do wątku głównego, zdeserializować i ponownie zgłosić? Spodziewam się, że aby to zadziałało, wszystkie wyjątki musiałyby pochodzić z tej samej klasy (lub przynajmniej z małego zestawu klas z instrukcją switch ponownie). Poza tym nie jestem pewien, czy można je serializować, po prostu głośno myślę.


Dlaczego trzeba go serializować, jeśli oba wątki są w tym samym procesie?
Nawaz

1
@Nawaz, ponieważ wyjątek prawdopodobnie zawiera odwołania do zmiennych lokalnych wątku, które nie są automatycznie dostępne dla innych wątków.
tvanfosson

2

W istocie nie ma dobrego i ogólnego sposobu przesyłania wyjątków z jednego wątku do drugiego.

Jeśli, tak jak powinno, wszystkie twoje wyjątki pochodzą ze std :: wyjątek, możesz mieć ogólny chwyt wyjątków najwyższego poziomu, który w jakiś sposób wyśle ​​wyjątek do głównego wątku, gdzie zostanie ponownie wyrzucony. Problem polega na tym, że tracisz punkt rzucania wyjątku. Prawdopodobnie możesz napisać kod zależny od kompilatora, aby uzyskać te informacje i przesłać je.

Jeśli nie wszystkie twoje wyjątki dziedziczą std :: wyjątek, to masz kłopoty i musisz napisać wiele haczyków najwyższego poziomu w swoim wątku ... ale rozwiązanie nadal działa.


1

Będziesz musiał wykonać ogólny catch dla wszystkich wyjątków w procesie roboczym (w tym wyjątków innych niż standardowe, takie jak naruszenia dostępu) i wysłać wiadomość z wątku roboczego (przypuszczam, że masz jakiś rodzaj wiadomości?) Do kontrolującego wątek, zawierający wskaźnik na żywo do wyjątku, i ponownie wyślij tam, tworząc kopię wyjątku. Następnie pracownik może uwolnić oryginalny przedmiot i wyjść.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.