Как я могу распространять исключения между потоками? - PullRequest
99 голосов
/ 24 октября 2008

У нас есть функция, в которую обращается один поток (мы называем это основным потоком). В теле функции мы порождаем несколько рабочих потоков для выполнения работы, интенсивно использующей процессор, ожидаем завершения всех потоков и возвращаем результат в главном потоке.

В результате вызывающая сторона может использовать функцию наивно, а внутри она будет использовать несколько ядер.

Пока все хорошо ..

Проблема, с которой мы сталкиваемся, связана с исключениями. Мы не хотим, чтобы исключения в рабочих потоках вызывали сбой приложения. Мы хотим, чтобы вызывающая функция могла перехватывать их в основном потоке. Мы должны перехватить исключения в рабочих потоках и распространить их на основной поток, чтобы они продолжали отматываться оттуда.

Как мы можем это сделать?

Лучшее, что я могу придумать:

  1. Поймать целый ряд исключений в наших рабочих потоках (std :: exception и несколько наших собственных).
  2. Запишите тип и сообщение об исключении.
  3. Иметь соответствующий оператор switch в главном потоке, который перебрасывает исключения любого типа, записанные в рабочем потоке.

Это имеет очевидный недостаток, заключающийся в поддержке только ограниченного набора типов исключений, и требует модификации при добавлении новых типов исключений.

Ответы [ 9 ]

72 голосов
/ 24 октября 2008

В настоящее время единственный способ portable состоит в том, чтобы написать предложения catch для всех типов исключений, которые вы могли бы передавать между потоками, сохранить информацию где-нибудь из этого предложения catch и затем использовать ее позже для повторного выброса исключение. Это подход, принятый Boost.Exception .

В C ++ 0x вы сможете перехватить исключение с помощью catch(...) и затем сохранить его в экземпляре std::exception_ptr с помощью std::current_exception(). Затем вы можете перебросить его позже из того же или другого потока с помощью std::rethrow_exception().

Если вы используете Microsoft Visual Studio 2005 или более позднюю версию, тогда библиотека just :: thread C ++ 0x поддерживает std::exception_ptr. (Отказ от ответственности: это мой продукт).

68 голосов
/ 07 сентября 2015

C ++ 11 представил тип exception_ptr, который позволяет передавать исключения между потоками:

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

Поскольку в вашем случае у вас есть несколько рабочих потоков, вам нужно оставить по одному exception_ptr для каждого из них.

Обратите внимание, что exception_ptr является общим ptr-подобным указателем, поэтому вам нужно будет оставить хотя бы один exception_ptr, указывающий на каждое исключение, иначе они будут освобождены.

Специфично для Microsoft: если вы используете исключения SEH (/EHa), пример кода также будет транспортировать исключения SEH, такие как нарушения прав доступа, что может быть не тем, что вы хотите.

9 голосов
/ 25 января 2013

Если вы используете C ++ 11, то std::future может делать именно то, что вам нужно: он может автоматически перехватывать исключения, которые попадают в начало рабочего потока, и передавать их родительскому объекту. поток в точке, которая называется std::future::get. (За кулисами это происходит точно так же, как в ответе @AnthonyWilliams; он только что был реализован для вас.)

Недостатком является то, что не существует стандартного способа "перестать заботиться" о std::future; даже его деструктор просто заблокирует, пока задача не будет выполнена. [РЕДАКТИРОВАТЬ, 2017: поведение деструктора блокировки является ошибкой только из псевдофьючерсов, возвращенных из std::async, которые вы никогда не должны использовать в любом случае. Нормальные фьючерсы не блокируют в своем деструкторе. Но вы все равно не можете «отменить» задачи, если используете std::future: задачи, выполняющие обещания, будут продолжать работать за кулисами, даже если никто больше не слушает ответ.] Вот игрушка пример, который может прояснить, что я имею в виду:

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

Я только что попытался написать пример для работы, используя std::thread и std::exception_ptr, но что-то не так с std::exception_ptr (используя libc ++), поэтому я пока не получил его на самом деле. (

[РЕДАКТИРОВАТЬ, 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!");
    }
}

Понятия не имею, что я делал неправильно в 2013 году, но я уверен, что это была моя вина.]

6 голосов
/ 24 октября 2008

Ваша проблема в том, что вы можете получить несколько исключений из нескольких потоков, поскольку каждый из них может выйти из строя, возможно, по разным причинам.

Я предполагаю, что основной поток каким-то образом ожидает окончания потоков, чтобы получить результаты, или регулярно проверяет прогресс других потоков, и этот доступ к общим данным синхронизируется.

Простое решение

Простым решением было бы перехватить все исключения в каждом потоке, записать их в общую переменную (в основном потоке).

После завершения всех потоков решите, что делать с исключениями. Это означает, что все остальные потоки продолжали свою обработку, что, возможно, не то, что вы хотите.

Комплексное решение

Более сложным решением является проверка каждого из ваших потоков в стратегических точках их выполнения, если исключение было вызвано из другого потока.

Если поток выдает исключение, он перехватывается перед выходом из потока, объект исключения копируется в некоторый контейнер в основном потоке (как в простом решении), а для некоторой общей логической переменной устанавливается значение true.

И когда другой поток проверяет это логическое значение, он видит, что выполнение должно быть прервано, и прерывается изящным способом.

Когда все потоки прервались, основной поток может обработать исключение по мере необходимости.

4 голосов
/ 24 октября 2008

Исключение, выброшенное из потока, не может быть перехвачено в родительском потоке. Потоки имеют разные контексты и стеки, и обычно родительскому потоку не требуется оставаться там и ждать завершения дочерних процессов, чтобы он мог перехватывать их исключения. В коде просто нет места для этого улова:

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

Вам нужно будет перехватывать исключения внутри каждого потока и интерпретировать состояние выхода из потоков основного потока, чтобы повторно выдать все исключения, которые вам могут понадобиться.

Кстати, в отсутствие перехвата в потоке это зависит от реализации, если разматывание стека будет выполнено вообще, то есть деструкторы ваших автоматических переменных могут даже не вызываться до вызова terminate. Некоторые компиляторы делают это, но это не обязательно.

3 голосов
/ 24 октября 2008

Не могли бы вы сериализовать исключение в рабочем потоке, передать его обратно в основной поток, десериализовать и снова выдать? Я ожидаю, что для того, чтобы это работало, все исключения должны быть производными от одного и того же класса (или, по крайней мере, небольшого набора классов с оператором switch снова). Кроме того, я не уверен, что они будут сериализуемыми, я просто думаю вслух.

2 голосов
/ 24 октября 2008

Действительно, нет хорошего и универсального способа передачи исключений из одного потока в другой.

Если, как и следовало ожидать, все ваши исключения происходят из std :: exception, то вы можете иметь общий ловушку исключений верхнего уровня, которая каким-то образом отправит исключение в основной поток, где оно будет снова выдано. Проблема в том, что вы теряете точку исключения. Вы, вероятно, можете написать зависимый от компилятора код, чтобы получить эту информацию и передать ее.

Если не все ваши исключения наследуют std :: exception, то у вас проблемы, и вам нужно написать много уловов верхнего уровня в вашем потоке ... но решение все еще остается в силе.

1 голос
/ 14 января 2010

См. http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html. Также возможно написать функцию-обертку для любой функции, которую вы вызываете для присоединения к дочернему потоку, которая автоматически перебрасывает (используя boost :: rethrow_exception) любое исключение, созданное дочерним потоком.

1 голос
/ 24 октября 2008

Вам нужно будет сделать общий перехват для всех исключений в работнике (включая исключения, не относящиеся к стандартному стандарту, например, нарушения доступа), и отправить сообщение из рабочего потока (я полагаю, у вас есть какой-то тип обмена сообщениями?) в управляющий поток, содержащий живой указатель на исключение, и перебросьте его, создав копию исключения. Затем работник может освободить исходный объект и выйти.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...