Come posso propagare le eccezioni tra i thread?


105

Abbiamo una funzione che viene chiamata da un singolo thread (lo chiamiamo thread principale). All'interno del corpo della funzione generiamo più thread di lavoro per svolgere un lavoro intensivo della CPU, attendere il completamento di tutti i thread, quindi restituire il risultato sul thread principale.

Il risultato è che il chiamante può utilizzare la funzione in modo ingenuo e internamente farà uso di più core.

Tutto bene finora ..

Il problema che abbiamo è trattare le eccezioni. Non vogliamo che le eccezioni sui thread di lavoro blocchino l'applicazione. Vogliamo che il chiamante della funzione sia in grado di intercettarli nel thread principale. Dobbiamo intercettare le eccezioni sui thread di lavoro e propagarle al thread principale per farle continuare a svincolarsi da lì.

Come possiamo farlo?

Il meglio che riesco a pensare è:

  1. Cattura un'intera varietà di eccezioni sui nostri thread di lavoro (std :: exception e alcuni dei nostri).
  2. Registra il tipo e il messaggio dell'eccezione.
  3. Avere un'istruzione switch corrispondente sul thread principale che rigenera le eccezioni di qualsiasi tipo sia stato registrato sul thread di lavoro.

Ciò ha l'ovvio svantaggio di supportare solo un insieme limitato di tipi di eccezione e richiederebbe modifiche ogni volta che venivano aggiunti nuovi tipi di eccezione.

Risposte:


89

C ++ 11 ha introdotto il exception_ptrtipo che consente di trasportare eccezioni tra thread:

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

Poiché nel tuo caso hai più thread di lavoro, dovrai mantenerne uno exception_ptrper ciascuno di essi.

Notare che exception_ptrè un puntatore simile a ptr condiviso, quindi sarà necessario mantenere almeno un puntatore exception_ptra ciascuna eccezione o verranno rilasciati.

Specifico per Microsoft: se usi le eccezioni SEH ( /EHa), il codice di esempio trasporterà anche le eccezioni SEH come le violazioni di accesso, che potrebbero non essere ciò che desideri.


Che dire dei thread multipli generati dal main? Se il primo thread colpisce un'eccezione ed esce, main () sarà in attesa al secondo thread join () che potrebbe essere eseguito per sempre. main () non riuscirà mai a testare teptr dopo i due join (). Sembra che tutti i thread debbano controllare periodicamente il teptr globale e uscire se appropriato. C'è un modo pulito per gestire questa situazione?
Cosmo

75

Attualmente, l'unico modo portabile è scrivere clausole catch per tutti i tipi di eccezioni che potresti voler trasferire tra thread, memorizzare le informazioni da qualche parte da quella clausola catch e quindi usarle in seguito per rilanciare un'eccezione. Questo è l'approccio adottato da Boost.Exception .

In C ++ 0x, sarai in grado di catturare un'eccezione catch(...)e quindi memorizzarla in un'istanza di std::exception_ptrutilizzo std::current_exception(). Puoi quindi lanciarlo di nuovo in seguito dallo stesso thread o da un thread diverso con std::rethrow_exception().

Se utilizzi Microsoft Visual Studio 2005 o successivo, la libreria di thread just :: thread C ++ 0x supportastd::exception_ptr . (Disclaimer: questo è il mio prodotto).


7
Questo fa ora parte di C ++ 11 ed è supportato da MSVS 2010; vedere msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde

7
È anche supportato da gcc 4.4+ su Linux.
Anthony Williams

Fantastico, c'è un link per un esempio di utilizzo: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke

11

Se stai usando C ++ 11, std::futurepotresti fare esattamente quello che stai cercando: può intercettare automaticamente le eccezioni che arrivano all'inizio del thread di lavoro e passarle al thread genitore nel punto in cuistd::future::get trova chiamato. (Dietro le quinte, questo accade esattamente come nella risposta di @AnthonyWilliams; è già stato implementato per te.)

Il lato negativo è che non esiste un modo standard per "smettere di preoccuparsi" a std::future; anche il suo distruttore si bloccherà fino al termine dell'attività. [EDIT, 2017: il comportamento del distruttore di blocco è una funzione sbagliata solo degli pseudo-futuri restituiti std::async, che non dovresti mai usare comunque. I futures normali non si bloccano nel loro distruttore. Ma non puoi ancora "annullare" le attività se le stai utilizzando std::future: le attività che soddisfano le promesse continueranno a essere eseguite dietro le quinte anche se nessuno ascolta più la risposta.] Ecco un esempio di giocattolo che potrebbe chiarire ciò che ho significare:

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

Ho appena provato a scrivere un esempio simile al lavoro usando std::threade std::exception_ptr, ma qualcosa non vastd::exception_ptr (usando libc ++) quindi non sono ancora riuscito a farlo funzionare davvero. :(

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

Non ho idea di cosa stessi facendo di sbagliato nel 2013, ma sono sicuro che sia stata colpa mia.]


Perché assegni il futuro a un nome fe poi a emplace_backesso? Non potresti semplicemente fare waitables.push_back(std::async(…));o sto trascurando qualcosa (si compila, la domanda è se potrebbe fuoriuscire, ma non vedo come)?
Konrad Rudolph

1
Inoltre, c'è un modo per sciogliere la pila abortendo i futures invece di waiting? Qualcosa del genere "non appena uno dei lavori fallisce, gli altri non contano più".
Konrad Rudolph

4 anni dopo, la mia risposta non è invecchiata bene. :) Re "Why": Penso che fosse solo per chiarezza (per mostrare che asyncrestituisce un futuro piuttosto che qualcos'altro). Re "Inoltre, c'è": Non in std::future, ma guarda il discorso di Sean Parent "Better Code: Concurrency" o il mio "Futures from Scratch" per diversi modi di implementarlo se non ti dispiace riscrivere l'intero STL per i principianti. :) Il termine chiave di ricerca è "cancellazione".
Quuxplusone

Grazie per la tua risposta. Darò sicuramente un'occhiata ai colloqui quando troverò un minuto.
Konrad Rudolph

1
Buona modifica 2017. Uguale a accettato, ma con un puntatore di eccezione con ambito. Lo metterei in alto e forse mi sbarazzerei anche del resto.
Nathan Cooper,

6

Il tuo problema è che potresti ricevere più eccezioni, da più thread, poiché ognuno potrebbe non riuscire, forse per motivi diversi.

Suppongo che il thread principale stia in qualche modo aspettando che i thread finiscano per recuperare i risultati o che controlli regolarmente l'avanzamento degli altri thread e che l'accesso ai dati condivisi sia sincronizzato.

Soluzione semplice

La soluzione semplice sarebbe catturare tutte le eccezioni in ogni thread, registrarle in una variabile condivisa (nel thread principale).

Una volta terminati tutti i thread, decidi cosa fare con le eccezioni. Ciò significa che tutti gli altri thread hanno continuato la loro elaborazione, che forse non è ciò che desideri.

Soluzione complessa

La soluzione più complessa è fare in modo che ciascuno dei tuoi thread controlli in punti strategici della loro esecuzione, se è stata generata un'eccezione da un altro thread.

Se un thread genera un'eccezione, viene catturato prima di uscire dal thread, l'oggetto eccezione viene copiato in un contenitore nel thread principale (come nella soluzione semplice) e alcune variabili booleane condivise vengono impostate su true.

E quando un altro thread verifica questo booleano, vede che l'esecuzione deve essere interrotta e si interrompe in modo grazioso.

Quando tutti i thread si sono interrotti, il thread principale può gestire l'eccezione secondo necessità.


4

Un'eccezione generata da un thread non sarà catturabile nel thread padre. I thread hanno contesti e stack diversi e in genere non è necessario che il thread padre rimanga lì e attenda che i figli finiscano, in modo che possa catturare le loro eccezioni. Semplicemente non c'è posto nel codice per quella cattura:

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

Dovrai catturare le eccezioni all'interno di ogni thread e interpretare lo stato di uscita dai thread nel thread principale per rilanciare eventuali eccezioni di cui potresti aver bisogno.

A proposito, in assenza di una cattura in un thread, è specifico dell'implementazione se verrà eseguito lo svolgimento dello stack, ovvero i distruttori delle variabili automatiche potrebbero non essere nemmeno chiamati prima che venga chiamato terminate. Alcuni compilatori lo fanno, ma non è richiesto.


3

È possibile serializzare l'eccezione nel thread di lavoro, trasmetterla al thread principale, deserializzarla e lanciarla di nuovo? Mi aspetto che, affinché funzioni, le eccezioni dovrebbero derivare tutte dalla stessa classe (o almeno un piccolo insieme di classi con di nuovo l'istruzione switch). Inoltre, non sono sicuro che sarebbero serializzabili, sto solo pensando ad alta voce.


Perché è necessario serializzarlo se entrambi i thread sono nello stesso processo?
Nawaz

1
@Nawaz perché l'eccezione probabilmente contiene riferimenti a variabili locali del thread che non sono automaticamente disponibili per altri thread.
tvanfosson

2

In effetti, non esiste un modo valido e generico per trasmettere eccezioni da un thread all'altro.

Se, come dovrebbe, tutte le tue eccezioni derivano da std :: exception, allora puoi avere un'eccezione generale di primo livello che in qualche modo invierà l'eccezione al thread principale dove verrà lanciata di nuovo. Il problema è che perdi il punto di lancio dell'eccezione. Probabilmente puoi scrivere codice dipendente dal compilatore per ottenere queste informazioni e trasmetterle però.

Se non tutte le eccezioni ereditano std :: exception, allora sei nei guai e devi scrivere un sacco di catch di primo livello nel tuo thread ... ma la soluzione è ancora valida.


1

Dovrai fare una cattura generica per tutte le eccezioni nel worker (comprese le eccezioni non standard, come le violazioni di accesso) e inviare un messaggio dal thread di lavoro (suppongo che tu abbia una sorta di messaggistica in atto?) Al controllo thread, contenente un puntatore attivo all'eccezione, e rilanciare lì creando una copia dell'eccezione. Quindi il lavoratore può liberare l'oggetto originale e uscire.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.