Як я можу поширювати винятки між потоками?


105

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

Результат полягає в тому, що абонент може використовувати функцію наївно, а внутрішньо він використовуватиме декілька ядер.

Все добре поки що ..

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

Як ми можемо це зробити?

Найкраще, що я можу придумати, це:

  1. Налаштуйте цілу низку винятків на наших робочих нитках (std :: виняток та кілька наших власних).
  2. Запишіть тип та повідомлення виключення.
  3. На головному потоці є відповідне твердження перемикача, яке переробляє винятки будь-якого типу, записаного на робочій нитці.

Це має очевидний недолік лише у підтримці обмеженого набору типів винятків, і він потребує модифікації, коли будуть додані нові типи винятків.

Відповіді:


89

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, такі як порушення доступу, які можуть бути не потрібними.


А що з кількома нитками, що породилися від основних? Якщо перший потік потрапить на виняток і закінчується, main () буде чекати на другому потоці join (), який може працювати назавжди. main () ніколи не потрапить на тест-тептр після двох приєднань (). Здається, всі потоки повинні періодично перевіряти глобальний teptr та виходити, якщо це доречно. Чи є чистий спосіб вирішити цю ситуацію?
Cosmo

75

В даний час єдиним портативним способом є написання пропозицій щодо вилову для всіх типів винятків, які ви можете перенести між потоками, зберігати інформацію десь із цього пункту вилучення, а потім використовувати їх для повторного скидання виключення. Такий підхід застосовується 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 . (Відмова: це мій продукт).


7
Зараз це частина C ++ 11 та підтримується MSVS 2010; див. msdn.microsoft.com/en-us/library/dd293602.aspx .
Йохан Реде

7
Він також підтримується gcc 4.4+ на Linux.
Ентоні Вільямс

Класно, є посилання для прикладу використання: en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke

11

Якщо ви використовуєте C ++ 11, то std::futureви можете робити саме те, що шукаєте: він може автоматично перехоплювати винятки, які вносять його до вершини робочої нитки, і передавати їх до батьківського потоку в точці, яка std::future::getє називається. (За лаштунками це відбувається саме так, як у відповіді @AnthonyWilliams; це просто реалізовано для вас.)

Суть в тому, що немає стандартного способу "припинити турботу" a 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не вдається (використовуючи libc ++), тому я ще не зрозумів, що це ще працює. :(

[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 році, але я впевнений, що це була моя вина.]


Чому ви присвоюєте майбутнє створює імені, fа потім emplace_backйого? Не могли б ви просто зробити waitables.push_back(std::async(…));чи я щось не помічаю (це компілюється, питання, чи може воно просочитися, але я не бачу як)?
Конрад Рудольф

1
Також, чи є спосіб розмотати стек, припинивши ф'ючерси замість waiting? Щось у руслі "щойно одна з робіт провалилася, інші більше не мають значення".
Конрад Рудольф

Через 4 роки моя відповідь не постаріла. :) Re "Чому": Я думаю, це було просто для ясності (щоб показати, що asyncповертає майбутнє, а не щось інше). Знову "Також, чи є": Не в std::future, але дивіться розмову Шона Батька "Кращий код: Паралельність" або мої "Майбутні з нуля" про різні способи реалізувати це, якщо ви не проти переписати всю STL для початківців. :) Ключовий пошуковий термін - "скасування".
Quuxplusone

Спасибі за Вашу відповідь. Я обов'язково погляну на переговори, коли знайду хвилину.
Конрад Рудольф

1
Хороша редакція 2017 року Те саме, що прийнято, але зі вказівкою на винятковий обхват Я б поставив це вгорі і, можливо, навіть позбувся від решти.
Натан Купер,

6

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

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

Просте рішення

Простим рішенням було б спіймати всі винятки у кожному потоці, записати їх у загальну змінну (в основний потік).

Після закінчення всіх потоків вирішіть, що робити з винятками. Це означає, що всі інші потоки продовжували їх обробку, що, можливо, не є тим, що ви хочете.

Комплексне рішення

Більш складним рішенням є те, щоб кожен з ваших потоків перевірити стратегічні точки їх виконання, якщо виняток було викинуто з іншої теми.

Якщо потік викидає виняток, він потрапляє перед виходом з потоку, об'єкт винятку копіюється в якийсь контейнер у головному потоці (як у простому рішенні), а деяка загальна булева змінна встановлюється в true.

І коли інша нитка тестує цю булеву форму, вона бачить, що виконання має бути перервано, а витончено перервано.

Коли всі потоки перервались, головна нитка може обробляти виняток за потребою.


4

Виняток, викинутий з потоку, не може бути зафіксований у батьківській нитці. Нитки мають різні контексти та стеки, і, як правило, батьківська нитка не вимагає залишатися там і чекати, коли діти закінчать, щоб він міг наздогнати їх винятки. Для цього вилову просто немає місця в коді:

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

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

BTW, у разі відсутності спіймання в потоці, це специфічно для реалізації, якщо розмотування стека буде зроблено взагалі, тобто деструктори автоматичних змінних можуть навіть не викликатися до виклику завершення. Деякі компілятори роблять це, але це не потрібно.


3

Чи можете ви серіалізувати виняток у робочій нитці, передати його назад в основний потік, десеріалізувати та перекинути його знову? Я очікую, що для цього для роботи винятки повинні були б походити з одного класу (або, принаймні, невеликого набору класів із повтором оператора переключення). Крім того, я не впевнений, що вони були б серіалізаційні, я просто вголос замислювався.


Чому потрібно її серіалізувати, якщо обидва потоки перебувають в одному процесі?
Наваз

1
@Nawaz, тому що виняток, ймовірно, містить посилання на локальні змінні, що не доступні автоматично для інших потоків.
tvanfosson

2

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

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

Якщо не всі ваші винятки успадковують std :: виключення, то ви перебуваєте в неприємності і вам доведеться записати багато вищого рівня улов у свою нитку ... але рішення все-таки має місце.


1

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


1

Див. Http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html . Можна також написати функцію обгортки будь-якої функції, яку ви закликаєте приєднати до дочірнього потоку, яка автоматично повторно перекидає (використовуючи boost :: rethrow_exception) будь-який виняток, що випускається дочірньою ниткою.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.