Как распространять исключения между потоками?

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

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

Все хорошо до сих пор..

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

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

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

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

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

Ответ 1

В С++ 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, такие как нарушения доступа, которые могут быть не такими, какие вы хотите.

Ответ 2

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

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

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

Ответ 3

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

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

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

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

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

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

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

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

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

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

Ответ 4

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

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

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

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

Ответ 5

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

Ответ 6

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

С другой стороны, нет стандартного способа "перестать заботиться" о std::future; даже его деструктор будет просто блокироваться, пока задача не будет выполнена. [EDIT, 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 (используя libС++), поэтому я еще не получил его на самом деле.: (

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

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

Ответ 7

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

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

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

Ответ 8

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

Ответ 9

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