Como posso propagar exceções entre threads?

105

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 é:

  1. Capture toda uma variedade de exceções em nossos threads de trabalho (std :: exception e algumas das nossas próprias).
  2. Registre o tipo e a mensagem da exceção.
  3. 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.

pauldoo
fonte

Respostas:

89

C ++ 11 introduziu o exception_ptrtipo que permite transportar exceções entre threads:

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

Porque no seu caso você tem vários threads de trabalho, você precisará manter um exception_ptrpara cada um deles.

Observe que exception_ptré um ponteiro compartilhado do tipo ptr, portanto, você precisará manter pelo menos um exception_ptrapontando para cada exceção ou eles serão liberados.

Específico da Microsoft: se você usar SEH Exceptions ( /EHa), o código de exemplo também transportará exceções SEH, como violações de acesso, que podem não ser o que você deseja.

Gerardo Hernandez
fonte
E quanto a vários threads gerados fora do principal? Se o primeiro thread atingir uma exceção e sair, main () estará esperando no segundo thread join (), que pode ser executado para sempre. main () nunca conseguiria testar teptr após as duas junções (). Parece que todos os threads precisam verificar periodicamente o teptr global e sair, se apropriado. Existe uma maneira limpa de lidar com essa situação?
Cosmo
75

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 de std::exception_ptrusing std::current_exception(). Você pode relançá-lo mais tarde a partir do mesmo ou de um thread diferente com std::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).

Anthony Williams
fonte
7
Isso agora faz parte do C ++ 11 e é compatível com o MSVS 2010; consulte msdn.microsoft.com/en-us/library/dd293602.aspx .
Johan Råde
7
Também é suportado pelo gcc 4.4+ no linux.
Anthony Williams
Legal, há um link para um exemplo de uso: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke
11

Se você estiver usando C ++ 11, então std::futurepode 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 que std::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 retornados std::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 usando std::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:

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

Eu apenas tentei escrever um exemplo de trabalho semelhante usando std::threade std::exception_ptr, mas algo está errado com std::exception_ptr(usando libc ++), então ainda não o fiz funcionar de verdade. :(

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

Não tenho ideia do que estava fazendo de errado em 2013, mas tenho certeza de que foi minha culpa.]

Quuxplusone
fonte
Por que você atribui o futuro cria a um nomeado fe depois a emplace_backele? Você não poderia simplesmente fazer waitables.push_back(std::async(…));ou estou esquecendo de algo (Compila, a questão é se pode vazar, mas não vejo como)?
Konrad Rudolph
1
Além disso, há uma maneira de desfazer a pilha abortando os futuros em vez de waitfazer? Algo como “assim que um dos trabalhos falhou, os outros não importam mais”.
Konrad Rudolph
4 anos depois, minha resposta não envelheceu bem. :) Re "Por quê": Acho que foi apenas para maior clareza (para mostrar que asyncretorna 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".
Quuxplusone
Obrigado pela sua resposta. Com certeza darei uma olhada nas palestras quando encontrar um minuto.
Konrad Rudolph
1
Boa edição de 2017. Igual ao aceito, mas com um ponteiro de exceção com escopo definido. Eu colocaria no topo e talvez até me livrasse do resto.
Nathan Cooper
6

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.

paercebal
fonte
4

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:

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

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.

n-alexander
fonte
3

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.

Tvanfosson
fonte
Por que é necessário serializá-lo se os dois threads estão no mesmo processo?
Nawaz
1
@Nawaz porque a exceção provavelmente tem referências a variáveis ​​locais de thread que não estão automaticamente disponíveis para outras threads.
tvanfosson
2

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.

PierreBdR
fonte
1

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.

anon6439
fonte