¿Cómo puedo propagar excepciones entre hilos?

105

Tenemos una función a la que llama un solo hilo (lo llamamos hilo principal). Dentro del cuerpo de la función, generamos varios subprocesos de trabajo para realizar un trabajo intensivo de la CPU, esperar a que finalicen todos los subprocesos y luego devolver el resultado en el subproceso principal.

El resultado es que la persona que llama puede usar la función de manera ingenua e internamente hará uso de múltiples núcleos.

Todo bien hasta ahora ..

El problema que tenemos es lidiar con las excepciones. No queremos que las excepciones en los hilos de trabajo bloqueen la aplicación. Queremos que la persona que llama a la función pueda capturarlos en el hilo principal. Debemos detectar excepciones en los subprocesos de trabajo y propagarlas al subproceso principal para que continúen desenvolviéndose desde allí.

¿Cómo podemos hacer esto?

Lo mejor que se me ocurre es:

  1. Detecte una gran variedad de excepciones en nuestros hilos de trabajo (std :: exception y algunas de las nuestras).
  2. Registre el tipo y mensaje de la excepción.
  3. Tenga una declaración de cambio correspondiente en el hilo principal que vuelva a generar excepciones de cualquier tipo que se haya registrado en el hilo de trabajo.

Esto tiene la desventaja obvia de que solo admite un conjunto limitado de tipos de excepciones y necesitaría modificaciones cada vez que se agregan nuevos tipos de excepciones.

Pauldoo
fuente

Respuestas:

89

C ++ 11 introdujo el exception_ptrtipo que permite transportar excepciones entre subprocesos:

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

Debido a que en su caso tiene varios subprocesos de trabajo, deberá mantener uno exception_ptrpara cada uno de ellos.

Tenga en cuenta que exception_ptres un puntero compartido similar a ptr, por lo que deberá mantener al menos uno que exception_ptrapunte a cada excepción o se liberarán.

Específico de Microsoft: si usa Excepciones SEH ( /EHa), el código de ejemplo también transportará excepciones SEH como violaciones de acceso, que pueden no ser lo que desea.

Gerardo Hernandez
fuente
¿Qué pasa con varios subprocesos generados fuera de main? Si el primer hilo encuentra una excepción y sale, main () estará esperando en el segundo hilo join () que podría ejecutarse para siempre. main () nunca podría probar teptr después de las dos combinaciones (). Parece que todos los subprocesos deben verificar periódicamente el teptr global y salir si corresponde. ¿Existe una forma limpia de manejar esta situación?
Cosmo
75

Actualmente, la única forma portátil es escribir cláusulas de captura para todos los tipos de excepciones que le gustaría transferir entre subprocesos, almacenar la información en algún lugar de esa cláusula de captura y luego usarla más tarde para volver a generar una excepción. Este es el enfoque adoptado por Boost.Exception .

En C ++ 0x, podrá detectar una excepción catch(...)y luego almacenarla en una instancia de std::exception_ptrusing std::current_exception(). A continuación, puede volver a lanzarlo más tarde desde el mismo hilo o desde otro diferente con std::rethrow_exception().

Si está utilizando Microsoft Visual Studio 2005 o posterior, entonces la biblioteca de subprocesos just :: thread C ++ 0x admite std::exception_ptr. (Descargo de responsabilidad: este es mi producto).

Anthony Williams
fuente
7
Esto ahora es parte de C ++ 11 y es compatible con MSVS 2010; consulte msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde
7
También es compatible con gcc 4.4+ en Linux.
Anthony Williams
Genial, hay un enlace para un ejemplo de uso: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke
11

Si está utilizando C ++ 11, entonces std::futurepodría hacer exactamente lo que está buscando: puede atrapar automáticamente las excepciones que llegan a la parte superior del hilo de trabajo y pasarlas al hilo principal en el punto que std::future::getes llamado. (Detrás de escena, esto sucede exactamente como en la respuesta de @AnthonyWilliams; ya se ha implementado para usted).

El lado negativo es que no existe una forma estándar de "dejar de preocuparse por" a std::future; incluso su destructor simplemente se bloqueará hasta que finalice la tarea. [EDITAR, 2017: El comportamiento del destructor de bloqueo es un error solo de los pseudo-futuros devueltos std::async, que nunca debe usar de todos modos. Los futuros normales no bloquean su destructor. Pero aún no puede "cancelar" tareas si está usando std::future: las tareas que cumplen la promesa continuarán ejecutándose detrás de escena incluso si nadie está escuchando la respuesta.] Aquí hay un ejemplo de juguete que podría aclarar lo que yo media:

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

Intenté escribir un ejemplo de trabajo similar usando std::thready std::exception_ptr, pero algo va mal con std::exception_ptr(usando libc ++), así que aún no he logrado que funcione. :(

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

No tengo idea de qué estaba haciendo mal en 2013, pero estoy seguro de que fue mi culpa.]

Quuxplusone
fuente
¿Por qué asigna el futuro crea a un nombre fy luego emplace_back? ¿No podrías simplemente hacer waitables.push_back(std::async(…));o estoy pasando por alto algo (se compila, la pregunta es si podría filtrarse, pero no veo cómo)?
Konrad Rudolph
1
Además, ¿hay alguna manera de deshacer la pila abortando los futuros en lugar de waiting? Algo en la línea “en cuanto falla uno de los trabajos, los demás ya no importan”.
Konrad Rudolph
4 años después, mi respuesta no ha envejecido bien. :) Re "Por qué": Creo que fue solo por claridad (para mostrar que asyncdevuelve un futuro en lugar de otra cosa). Re "Also, is there": No in std::future, pero vea la charla de Sean Parent "Better Code: Concurrency" o mi "Futures from Scratch" para conocer diferentes formas de implementar eso si no le importa reescribir todo el STL para empezar. :) El término de búsqueda clave es "cancelación".
Quuxplusone
Gracias por su respuesta. Definitivamente echaré un vistazo a las charlas cuando encuentre un minuto.
Konrad Rudolph
1
Buena edición de 2017. Igual que el aceptado, pero con un puntero de excepción con ámbito. Lo pondría en la parte superior y tal vez incluso me desharía del resto.
Nathan Cooper
6

Su problema es que podría recibir múltiples excepciones, de múltiples subprocesos, ya que cada uno podría fallar, quizás por diferentes razones.

Supongo que el hilo principal de alguna manera está esperando a que los hilos terminen para recuperar los resultados, o verificando regularmente el progreso de los otros hilos, y que el acceso a los datos compartidos está sincronizado.

Solución simple

La solución simple sería capturar todas las excepciones en cada hilo, registrarlas en una variable compartida (en el hilo principal).

Una vez que terminen todos los hilos, decida qué hacer con las excepciones. Esto significa que todos los demás subprocesos continuaron su procesamiento, que quizás no sea lo que desea.

Solución compleja

La solución más compleja es que cada uno de sus subprocesos se verifique en puntos estratégicos de su ejecución, si se lanzó una excepción desde otro subproceso.

Si un hilo produce una excepción, se detecta antes de salir del hilo, el objeto de excepción se copia en algún contenedor del hilo principal (como en la solución simple) y alguna variable booleana compartida se establece en verdadera.

Y cuando otro hilo prueba este booleano, ve que la ejecución debe ser abortada y aborta de una manera elegante.

Cuando todos los subprocesos abortaron, el subproceso principal puede manejar la excepción según sea necesario.

paercebal
fuente
4

Una excepción lanzada desde un hilo no será detectable en el hilo principal. Los subprocesos tienen diferentes contextos y pilas y, por lo general, no se requiere que el subproceso padre permanezca allí y espere a que los hijos terminen, para poder detectar sus excepciones. Simplemente no hay lugar en el código para esa captura:

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

Deberá detectar las excepciones dentro de cada hilo e interpretar el estado de salida de los hilos en el hilo principal para volver a lanzar cualquier excepción que pueda necesitar.

Por cierto, en la ausencia de una captura en un hilo, es específico de la implementación si el desenrollado de la pila se realizará, es decir, es posible que los destructores de sus variables automáticas ni siquiera se llamen antes de que termine. Algunos compiladores hacen eso, pero no es obligatorio.

n-alexander
fuente
3

¿Podría serializar la excepción en el hilo de trabajo, transmitirla al hilo principal, deserializar y lanzarla de nuevo? Espero que para que esto funcione, todas las excepciones tendrían que derivar de la misma clase (o al menos un pequeño conjunto de clases con la instrucción de cambio nuevamente). Además, no estoy seguro de que se puedan serializar, solo estoy pensando en voz alta.

tvanfosson
fuente
¿Por qué es necesario serializarlo si ambos hilos están en el mismo proceso?
Nawaz
1
@Nawaz porque la excepción probablemente tiene referencias a variables locales de subprocesos que no están disponibles automáticamente para otros subprocesos.
tvanfosson
2

De hecho, no existe una forma buena y genérica de transmitir excepciones de un hilo al siguiente.

Si, como debería, todas sus excepciones derivan de std :: exception, entonces puede tener una captura de excepción general de nivel superior que de alguna manera enviará la excepción al hilo principal donde se lanzará nuevamente. El problema es que pierdes el punto de partida de la excepción. Probablemente pueda escribir código dependiente del compilador para obtener esta información y transmitirla.

Si no todas sus excepciones heredan std :: exception, entonces está en problemas y tiene que escribir muchas capturas de nivel superior en su hilo ... pero la solución aún se mantiene.

PierreBdR
fuente
1

Deberá realizar una captura genérica para todas las excepciones en el trabajador (incluidas las excepciones que no son estándar, como las infracciones de acceso) y enviar un mensaje desde el hilo del trabajador (¿supongo que tiene algún tipo de mensaje en su lugar?) Al controlador hilo, que contiene un puntero en vivo a la excepción, y volver a lanzar allí creando una copia de la excepción. Luego, el trabajador puede liberar el objeto original y salir.

anon6439
fuente