Деструктор concurrency::task вызывает прерывание вызова в допустимом сценарии использования

Не могли бы вы сказать мне, если подход, который я использую для обработки сценария использования, является недействительным, и если да, то какой правильный способ обработки:

task<int> do_work(int param)
{
    // runs some work on a separate thread, returns task with result or throws exception on failure
}

void foo()
{
    try
    {
        auto result_1 = do_work(10000);
        auto result_2 = do_work(20000);

        // do some extra work

        process(result_1.get(), result_2.get());
    }
    catch (...)
    {
        // logs the failure details
    }
}

Таким образом, код пытается выполнить два задания параллельно, а затем обработать результаты. Если одно из заданий вызывает исключение, позвоните task::get перебросит исключение. Проблема возникает, если обе задачи выдают исключение. В этом случае первый звонок task::get приведет к размотке стека, поэтому деструктор второго task будет вызван и, в свою очередь, вызовет повторное появление еще одного исключения во время размотки стека, что вызовет вызов 'abort'.

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

1 ответ

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

Более упрощенный код показывает, что std::terminate вызван, потому что исключение, брошенное в задачу, не обрабатывается. Раскомментировать result.get() предотвратит звонок std::terminate, как task::get перебросит исключение.

#include <pplx/pplx.h>
#include <pplx/pplxtasks.h>
#include <iostream>

int main(int argc, char* argv[])
{
    try
    {
        auto result = pplx::create_task([] ()-> int
        {
            throw std::exception("task failed");
        });

        // actually need wait here till the exception is thrown, e.g.
        // result.wait(), but this will re-throw the exception making this a valid use-case

        std::cout << &result << std::endl; // use it
        //std::cout << result.get() << std::endl;
    }
    catch (std::exception const& ex)
    {
        std::cout << ex.what() << std::endl;
    }

    return 0;
}

взглянуть на предложение в pplx::details::_ExceptionHandler::~_ExceptionHolder()

//pplxwin.h
#define _REPORT_PPLTASK_UNOBSERVED_EXCEPTION() do { \
    __debugbreak(); \
    std::terminate(); \
} while(false)


//pplxtasks.h
pplx::details::_ExceptionHandler::~_ExceptionHolder()
{
    if (_M_exceptionObserved == 0)
    {
        // If you are trapped here, it means an exception thrown in task chain didn't get handled.
        // Please add task-based continuation to handle all exceptions coming from tasks.
        // this->_M_stackTrace keeps the creation callstack of the task generates this exception.
        _REPORT_PPLTASK_UNOBSERVED_EXCEPTION();
    }
}

В оригинальном коде первый вызов task::get вызывает исключение, выброшенное в этой задаче, что, очевидно, предотвращает второй вызов task::get таким образом, исключение из второй задачи не обрабатывается (остается "ненаблюдаемым").

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

Деструктор второй задачи не перебрасывает исключение, он просто вызывает std::terminate() (который вызывает std::abort())

увидеть. Обработка исключений в параллельной среде выполнения

Другие вопросы по тегам