c++ - tipos - ¿Cómo puedo propagar excepciones entre hilos?
tipos de excepciones en programacion (9)
Tenemos una función a la que llama un solo hilo (le damos el nombre al hilo principal). Dentro del cuerpo de la función generamos múltiples subprocesos de trabajo para hacer un trabajo intensivo de CPU, esperamos que todos los subprocesos finalicen y luego devolvemos el resultado en el subproceso principal.
El resultado es que la persona que llama puede usar la función ingenuamente, e internamente hará uso de múltiples núcleos.
Todo bien hasta ahora ..
El problema que tenemos es tratar con 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 atraparlos en el hilo principal. Debemos capturar excepciones en los subprocesos de trabajo y propagarlos a través del subproceso principal para que continúen desenvolviéndose desde allí.
¿Cómo podemos hacer esto?
Lo mejor que puedo pensar es:
- Captura una gran variedad de excepciones en nuestros hilos de trabajo (std :: exception y algunos de los nuestros).
- Registre el tipo y el mensaje de la excepción.
- Tenga una instrucción de cambio correspondiente en el hilo principal que vuelva a generar excepciones de cualquier tipo que se grabó en el hilo del trabajador.
Esto tiene la desventaja obvia de que solo es compatible con un conjunto limitado de tipos de excepciones, y necesitaría modificaciones cada vez que se agregaran nuevos tipos de excepciones.
¿Podría serializar la excepción en el hilo de trabajo, transmitirla de vuelta al hilo principal, deserializar y lanzarla de nuevo? Espero que para que esto funcione 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 de nuevo). Además, no estoy seguro de que puedan ser serializables, solo estoy pensando en voz alta.
Actualmente, la única forma portátil es escribir cláusulas de captura para todos los tipos de excepciones que le gustaría transferir entre hilos, almacenar la información en algún lugar de esa cláusula de captura y luego usarla más adelante para volver a lanzar una excepción. Este es el enfoque adoptado por Boost.Exception .
En C ++ 0x, podrá capturar una excepción con catch(...)
y luego almacenarla en una instancia de std::exception_ptr
usando std::current_exception()
. A continuación, puede volver a lanzarlo más tarde desde el mismo subproceso o con un subproceso diferente con std::rethrow_exception()
.
Si está utilizando Microsoft Visual Studio 2005 o posterior, la biblioteca de hilos just :: thread C ++ 0x admite std::exception_ptr
. (Descargo de responsabilidad: este es mi producto).
C ++ 11 introdujo el tipo exception_ptr que permite transportar excepciones entre hilos:
#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 múltiples hilos de trabajo, necesitará mantener un exception_ptr para cada uno de ellos.
Tenga en cuenta que exception_ptr es un puntero compartido tipo ptr, por lo que deberá mantener al menos un exception_ptr apuntando a cada excepción o se publicarán.
Específico de Microsoft: si usa Excepciones de SEH (/ EHa), el código de ejemplo también transportará excepciones de SEH, como infracciones de acceso, que pueden no ser las que usted desea.
De hecho, no existe una forma buena y genérica de transmitir excepciones de un hilo al siguiente.
Si, como debería ser, todas sus excepciones derivan de std :: exception, entonces puede tener una captura de excepción general de nivel superior que de alguna manera envíe la excepción al hilo principal donde se lanzará nuevamente. El problema es que pierdes el punto de lanzamiento de la excepción. Sin embargo, probablemente pueda escribir código dependiente del compilador para obtener esta información y transmitirla.
Si no toda su excepción hereda std :: exception, entonces tiene problemas y tiene que escribir muchas capturas de alto nivel en su hilo ... pero la solución aún se mantiene.
Si usa C ++ 11, std::future
puede hacer exactamente lo que está buscando: puede atrapar automágicamente excepciones que llegan a la parte superior del hilo de trabajo, y pasarlas al hilo principal en el punto que se llama std::future::get
. (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 hay una forma estándar de "dejar de preocuparse por" un estándar std::future
; incluso su destructor simplemente bloqueará hasta que la tarea esté hecha. [EDITAR, 2017: el comportamiento de bloqueo-destructor es una característica errónea solo de los pseudo-futuros devueltos por std::async
, que nunca se deben usar de todos modos. Los futuros normales no bloquean en su destructor. Pero aún no puede "cancelar" las tareas si está utilizando std::future
: las tareas que cumplen las promesas continuarán ejecutándose tras bastidores, incluso si ya nadie está escuchando la respuesta.] Aquí hay un ejemplo de juguete que podría aclarar lo que quiero decir:
#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 similar al trabajo usando std::thread
y std::exception_ptr
, pero algo anda mal con std::exception_ptr
(usando libc ++), así que todavía no lo he conseguido. :(
[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!");
}
}
No tengo idea de lo que estaba haciendo mal en 2013, pero estoy seguro de que fue mi culpa.]
Su problema es que podría recibir múltiples excepciones, desde múltiples hilos, ya que cada uno podría fallar, quizás por diferentes motivos.
Estoy asumiendo que el hilo principal de alguna manera está esperando que los hilos terminen para recuperar los resultados, o verifique 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 todos los hilos terminen, decida qué hacer con las excepciones. Esto significa que todos los otros hilos continuaron su procesamiento, que tal vez, no es lo que desea.
Solución compleja
La solución más compleja es hacer que cada uno de sus hilos verifique en los puntos estratégicos de su ejecución, si se lanzó una excepción desde otro hilo.
Si un hilo arroja una excepción, se captura antes de salir del hilo, el objeto de excepción se copia en algún contenedor en el hilo principal (como en la solución simple) y alguna variable booleana compartida se establece en verdadero.
Y cuando otro hilo prueba este booleano, ve que la ejecución se abortará y aborta de una manera elegante.
Cuando se canceló todo el hilo, el hilo principal puede manejar la excepción según sea necesario.
Tendrá que realizar una captura genérica de todas las excepciones del trabajador (incluidas excepciones no estándar, como infracciones de acceso) y enviar un mensaje del hilo de trabajo (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í al crear una copia de la excepción. Entonces el trabajador puede liberar el objeto original y salir.
Una excepción lanzada desde un hilo no será capturable en el hilo padre. Los subprocesos tienen diferentes contextos y apilamientos, y generalmente no se requiere que el subproceso principal permanezca allí y espere a que los hijos terminen, para que pueda 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 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 ausencia de una captura en un hilo, es específico de la implementación si se realiza el desenrollado de la pila, es decir, los destructores de sus variables automáticas pueden incluso no ser llamados antes de que se llame a terminar. Algunos compiladores hacen eso, pero no es obligatorio.
Ver http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html . También es posible escribir una función contenedora de cualquier función que llame para unir un subproceso secundario, que reenvía automáticamente (utilizando boost :: rethrow_exception) cualquier excepción emitida por un subproceso secundario.