Temos uma função para a qual um único thread chama (chamamos isso de thread principal). Dentro do corpo da função, geramos vários threads de trabalho para fazer um trabalho intensivo de CPU, esperamos que todos os threads terminem e, em seguida, retornamos o resultado no thread principal.
O resultado é que o chamador pode usar a função ingenuamente e, internamente, fará uso de vários núcleos.
Tudo bem até agora ..
O problema que temos é lidar com exceções. Não queremos exceções nos threads de trabalho para travar o aplicativo. Queremos que o chamador da função possa capturá-los no thread principal. Devemos capturar exceções nos threads de trabalho e propagá-los para o thread principal para que continuem sendo desenrolados a partir daí.
Como podemos fazer isso?
O melhor que posso pensar é:
- Capture toda uma variedade de exceções em nossos threads de trabalho (std :: exception e algumas das nossas próprias).
- Registre o tipo e a mensagem da exceção.
- Tenha uma instrução switch correspondente no thread principal que relança exceções de qualquer tipo que tenha sido registrado no thread de trabalho.
Isso tem a desvantagem óbvia de suportar apenas um conjunto limitado de tipos de exceção e precisaria de modificação sempre que novos tipos de exceção fossem adicionados.
fonte
Atualmente, a única maneira portátil é escrever cláusulas catch para todos os tipos de exceções que você gostaria de transferir entre threads, armazenar as informações em algum lugar dessa cláusula catch e usá-las posteriormente para relançar uma exceção. Essa é a abordagem adotada por Boost.Exception .
Em C ++ 0x, você poderá capturar uma exceção com
catch(...)
e armazená-la em uma instância destd::exception_ptr
usingstd::current_exception()
. Você pode relançá-lo mais tarde a partir do mesmo ou de um thread diferente comstd::rethrow_exception()
.Se você estiver usando o Microsoft Visual Studio 2005 ou posterior, a biblioteca de threads just :: thread C ++ 0x oferece suporte
std::exception_ptr
. (Aviso: este é meu produto).fonte
Se você estiver usando C ++ 11, então
std::future
pode fazer exatamente o que você está procurando: ele pode capturar automaticamente as exceções que chegam ao topo do thread de trabalho e passá-las para o thread pai no ponto questd::future::get
é chamado. (Nos bastidores, isso acontece exatamente como na resposta de @AnthonyWilliams; ela já foi implementada para você.)O lado ruim é que não há uma maneira padrão de "parar de se preocupar com" a
std::future
; até mesmo seu destruidor simplesmente bloqueará até que a tarefa seja concluída. [EDIT, 2017: O comportamento do destruidor de bloqueio é uma característica incorreta apenas dos pseudo-futuros retornadosstd::async
, os quais você nunca deve usar de qualquer maneira. Futuros normais não bloqueiam em seu destruidor. Mas você ainda não pode "cancelar" tarefas se estiver usandostd::future
: a (s) tarefa (s) de cumprimento de promessa continuarão sendo executados nos bastidores, mesmo que ninguém esteja mais ouvindo a resposta.] Aqui está um exemplo de brinquedo que pode esclarecer o que eu significar:Eu apenas tentei escrever um exemplo de trabalho semelhante usando
std::thread
estd::exception_ptr
, mas algo está errado comstd::exception_ptr
(usando libc ++), então ainda não o fiz funcionar de verdade. :([EDITAR, 2017:
Não tenho ideia do que estava fazendo de errado em 2013, mas tenho certeza de que foi minha culpa.]
fonte
f
e depois aemplace_back
ele? Você não poderia simplesmente fazerwaitables.push_back(std::async(…));
ou estou esquecendo de algo (Compila, a questão é se pode vazar, mas não vejo como)?wait
fazer? Algo como “assim que um dos trabalhos falhou, os outros não importam mais”.async
retorna um futuro ao invés de outra coisa). Re "Além disso, existe": Não estástd::future
, mas veja a palestra de Sean Parent "Código Melhor: Simultaneidade" ou meu "Futuros do zero" para diferentes maneiras de implementar isso, se você não se importar em reescrever todo o STL para começar. :) O principal termo de pesquisa é "cancelamento".O problema é que você pode receber várias exceções, de vários threads, pois cada um pode falhar, talvez por motivos diferentes.
Estou assumindo que o thread principal está de alguma forma esperando que os threads terminem para recuperar os resultados, ou verificando regularmente o progresso dos outros threads, e que o acesso aos dados compartilhados seja sincronizado.
Solução simples
A solução simples seria capturar todas as exceções em cada thread, gravá-las em uma variável compartilhada (na thread principal).
Assim que todos os threads terminarem, decida o que fazer com as exceções. Isso significa que todos os outros threads continuaram seu processamento, o que talvez não seja o que você deseja.
Solução complexa
A solução mais complexa é fazer com que cada um de seus threads verifique em pontos estratégicos de sua execução, se uma exceção foi lançada de outro thread.
Se um encadeamento lançar uma exceção, ele será capturado antes de sair do encadeamento, o objeto de exceção será copiado para algum contêiner no encadeamento principal (como na solução simples) e alguma variável booleana compartilhada será definida como true.
E quando outro thread testa este booleano, ele vê que a execução deve ser abortada e aborta de uma forma elegante.
Quando todo o encadeamento foi abortado, o encadeamento principal pode tratar a exceção conforme necessário.
fonte
Uma exceção lançada de um encadeamento não será capturável no encadeamento pai. Threads têm diferentes contextos e pilhas e, geralmente, o thread pai não precisa ficar lá e esperar que os filhos terminem, para que ele possa capturar suas exceções. Simplesmente não há lugar no código para essa captura:
Você precisará capturar exceções dentro de cada thread e interpretar o status de saída de threads no thread principal para relançar quaisquer exceções que você possa precisar.
BTW, na ausência de um catch em um thread, é específico da implementação se o desenrolamento da pilha for feito, ou seja, os destruidores de suas variáveis automáticas podem nem mesmo ser chamados antes de terminate ser chamado. Alguns compiladores fazem isso, mas não é obrigatório.
fonte
Você poderia serializar a exceção no thread de trabalho, transmiti-la de volta para o thread principal, desserializar e lançar novamente? Espero que para que isso funcione, todas as exceções devem derivar da mesma classe (ou pelo menos um pequeno conjunto de classes com a instrução switch novamente). Além disso, não tenho certeza se eles seriam serializáveis, estou apenas pensando em voz alta.
fonte
Na verdade, não existe uma maneira boa e genérica de transmitir exceções de um thread para o outro.
Se, como deveria, todas as suas exceções derivam de std :: exception, então você pode ter uma captura de exceção geral de nível superior que de alguma forma enviará a exceção para o thread principal onde ela será lançada novamente. O problema é que você perde o ponto de partida da exceção. Provavelmente, você pode escrever código dependente do compilador para obter essas informações e transmiti-las.
Se nem todas as suas exceções herdarem std :: exception, então você está em apuros e terá que escrever um monte de catch de nível superior em seu thread ... mas a solução ainda se mantém.
fonte
Você precisará fazer uma captura genérica para todas as exceções no trabalhador (incluindo exceções não-padrão, como violações de acesso) e enviar uma mensagem do thread de trabalho (suponho que você tenha algum tipo de mensagem em vigor?) Para o controlador thread, contendo um ponteiro ativo para a exceção, e relançá-la criando uma cópia da exceção. Então o trabalhador pode liberar o objeto original e sair.
fonte
Consulte http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html . Também é possível escrever uma função de invólucro de qualquer função que você chamar para ingressar em um thread filho, que relança automaticamente (usando boost :: rethrow_exception) qualquer exceção emitida por um thread filho.
fonte