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

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

Основные моменты

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

  • По возможности окружите каждый вызов параллелизма::task::get и concurrency::task:wait с блоком try/catch для обработки ошибок, которые можно восстановить. Среда выполнения завершает приложение, если задача создает исключение и это исключение не перехватывается задачей, одним из ее продолжений или основным приложением.

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

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

  • Среда выполнения создает параллелизм::task_canceled при вызове параллелизма::task::get и отменяется эта задача.

  • Среда выполнения не управляет исключениями для легковесных задач и агентов.

В этом документе

Задачи и продолжения

В этом разделе описывается, как среда выполнения обрабатывает исключения, создаваемые параллелизмом ::task и их продолжениеми. Дополнительные сведения о модели задач и продолжения см. в разделе "Параллелизм задач".

При возникновении исключения в тексте рабочей функции, передаваемой task объекту, среда выполнения сохраняет это исключение и маршалирует его в контекст, который вызывает параллелизм::task::get или concurrency::task:task:wait. Параллелизм задач документа описывает продолжение на основе задач и продолжение на основе значений, но для суммирования продолжение на основе значений принимает параметр типаT, а продолжение на основе задач принимает параметр типаtask<T>. Если задача, которая создает исключение, имеет одно или несколько продолжений, основанных на значении, эти продолжения не ставятся в очередь для запуска. Это демонстрируется в приведенном ниже примере.

// eh-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    wcout << L"Running a task..." << endl;
    // Create a task that throws.
    auto t = create_task([]
    {
        throw exception();
    });
    
    // Create a continuation that prints its input value.
    auto continuation = t.then([]
    {
        // We do not expect this task to run because
        // the antecedent task threw.
        wcout << L"In continuation task..." << endl;
    });

    // Wait for the continuation to finish and handle any 
    // error that occurs.
    try
    {
        wcout << L"Waiting for tasks to finish..." << endl;
        continuation.wait();

        // Alternatively, call get() to produce the same result.
        //continuation.get();
    }
    catch (const exception& e)
    {
        wcout << L"Caught exception." << endl;
    }
}
/* Output:
    Running a task...
    Waiting for tasks to finish...
    Caught exception.
*/

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

// eh-continuations.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{    
    wcout << L"Running a task..." << endl;
    // Create a task that throws.
    auto t = create_task([]() -> int
    {
        throw exception();
        return 42;
    });

    //
    // Attach two continuations to the task. The first continuation is  
    // value-based; the second is task-based.

    // Value-based continuation.
    auto c1 = t.then([](int n)
    {
        // We don't expect to get here because the antecedent 
        // task always throws.
        wcout << L"Received " << n << L'.' << endl;
    });

    // Task-based continuation.
    auto c2 = t.then([](task<int> previousTask)
    {
        // We do expect to get here because task-based continuations
        // are scheduled even when the antecedent task throws.
        try
        {
            wcout << L"Received " << previousTask.get() << L'.' << endl;
        }
        catch (const exception& e)
        {
            wcout << L"Caught exception from previous task." << endl;
        }
    });

    // Wait for the continuations to finish.
    try
    {
        wcout << L"Waiting for tasks to finish..." << endl;
        (c1 && c2).wait();
    }
    catch (const exception& e)
    {
        wcout << L"Caught exception while waiting for all tasks to finish." << endl;
    }
}
/* Output:
    Running a task...
    Waiting for tasks to finish...
    Caught exception from previous task.
    Caught exception while waiting for all tasks to finish.
*/

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

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

// eh-task-chain.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    int n = 1;
    create_task([n]
    {
        wcout << L"In first task. n = ";
        wcout << n << endl;
        
        return n * 2;

    }).then([](int n)
    {
        wcout << L"In second task. n = ";
        wcout << n << endl;

        return n * 2;

    }).then([](int n)
    {
        wcout << L"In third task. n = ";
        wcout << n << endl;

        // This task throws.
        throw exception();
        // Not reached.
        return n * 2;

    }).then([](int n)
    {
        // This continuation is not run because the previous task throws.
        wcout << L"In fourth task. n = ";
        wcout << n << endl;

        return n * 2;

    }).then([](task<int> previousTask)
    {
        // This continuation is run because it is task-based.
        try
        {
            // The call to task::get rethrows the exception.
            wcout << L"In final task. result = ";
            wcout << previousTask.get() << endl;
        }
        catch (const exception&)
        {
            wcout << L"<exception>" << endl;
        }
    }).wait();
}
/* Output:
    In first task. n = 1
    In second task. n = 2
    In third task. n = 4
    In final task. result = <exception>
*/

Совет

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

concurrency::task_canceled — важный тип исключения среды выполнения, который относится к task. Среда выполнения создает task_canceled при вызове task::get, если эта задача отменена. (И наоборот, task::wait возвращает task_status::отменено и не вызывается.) Вы можете перехватывать и обрабатывать это исключение из продолжения на основе задач или при вызове task::get. Дополнительные сведения об отмене задач см. в разделе "Отмена" в PPL.

Внимание

Никогда не вызывайте исключение task_canceled из своего кода. Вместо этого вызовите параллелизм::cancel_current_task .

Среда выполнения завершает приложение, если задача создает исключение и это исключение не перехватывается задачей, одним из ее продолжений или основным приложением. Если приложение аварийно завершается, можно настроить Visual Studio, чтобы прерывать выполнение при создании исключений C++. После выяснения расположения необработанного исключения используйте продолжение на основе задачи, чтобы обработать его.

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

[В начало]

Группы задач и параллельные алгоритмы

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

Внимание

Убедитесь, что вы понимаете, что исключения имеют последствия для зависимых задач. Рекомендации по использованию обработки исключений с задачами или параллельными алгоритмами см . в разделе "Общие сведения о том, как отмена и обработка исключений влияют на уничтожение объектов" в разделе "Рекомендации" в разделе "Библиотека параллельных шаблонов".

Дополнительные сведения о группах задач см. в разделе "Параллелизм задач". Дополнительные сведения о параллельных алгоритмах см. в разделе "Параллельные алгоритмы".

При возникновении исключения в тексте рабочей функции, передаваемой в параллелизм:::task_group или объект параллелизма:::structured_task_group, среда выполнения сохраняет это исключение и маршалирует его в контекст, который вызывает параллелизм::task_group::wait, concurrency::structured_task_group::wait, wait, concurrency::wait, concurrency::task_group:run_and_wait или параллелизм:: structured_task_group::run_and_wait. Среда выполнения также останавливает все активные задачи, которые находятся в группе задач (в том числе в дочерних группах задач) и не карта все задачи, которые еще не запущены.

В следующем примере показана базовая структура рабочей функции, которая вызывает исключение. В примере используется task_group объект для печати значений двух point объектов параллельно. print_point Рабочая функция выводит значения point объекта в консоль. Рабочая функция создает исключение, если входное значение равно NULL. Среда выполнения сохраняет это исключение и маршалирует его в контекст, который вызывает task_group::wait.

// eh-task-group.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

// Defines a basic point with X and Y coordinates.
struct point
{
   int X;
   int Y;
};

// Prints the provided point object to the console.
void print_point(point* pt)
{
   // Throw an exception if the value is NULL.
   if (pt == NULL)
   {
      throw exception("point is NULL.");
   }

   // Otherwise, print the values of the point.
   wstringstream ss;
   ss << L"X = " << pt->X << L", Y = " << pt->Y << endl;
   wcout << ss.str();
}

int wmain()
{
   // Create a few point objects.
   point pt = {15, 30};
   point* pt1 = &pt;
   point* pt2 = NULL;

   // Use a task group to print the values of the points.
   task_group tasks;

   tasks.run([&] {
      print_point(pt1);
   });

   tasks.run([&] {
      print_point(pt2);
   });

   // Wait for the tasks to finish. If any task throws an exception,
   // the runtime marshals it to the call to wait.
   try
   {
      tasks.wait();
   }
   catch (const exception& e)
   {
      wcerr << L"Caught exception: " << e.what() << endl;
   }
}

В этом примере формируются следующие данные:

X = 15, Y = 30Caught exception: point is NULL.

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

[В начало]

Исключения, создаваемые средой выполнения

Исключение может быть результатом вызова среды выполнения. Большинство типов исключений, за исключением параллелизма::task_canceled и параллелизма::operation_timed_out, указывают на ошибку программирования. Эти ошибки обычно не возвращаются, поэтому не следует перехватываться или обрабатываться кодом приложения. Мы рекомендуем перехватывать или обрабатывать только неустранимые ошибки в коде приложения, если необходимо диагностировать ошибки программирования. Однако понимание типов исключений, определенных средой выполнения, помогает диагностировать ошибки программирования.

Механизм обработки исключений совпадает с исключениями, которые создаются средой выполнения в качестве исключений, создаваемых рабочими функциями. Например, функция параллелизма::receive вызываетсяoperation_timed_out, если она не получает сообщение в течение указанного периода времени. Если receive создается исключение в рабочей функции, передаваемой в группу задач, среда выполнения сохраняет это исключение и маршалирует его в контекст, который вызывает task_group::wait, structured_task_group::waittask_group::run_and_waitилиstructured_task_group::run_and_wait.

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

// eh-time-out.cpp
// compile with: /EHsc
#include <agents.h>
#include <ppl.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
   single_assignment<int> buffer;
   int result;

   try
   {
      // Run two tasks in parallel.
      parallel_invoke(
         // This task waits 5 seconds and then sends a message to 
         // the message buffer.
         [&] {
            wait(5000); 
            send(buffer, 42);
         },
         // This task waits 3 seconds to receive a message.
         // The receive function throws operation_timed_out if it does 
         // not receive a message in the specified time period.
         [&] {
            result = receive(buffer, 3000);
         }
      );

      // Print the result.
      wcout << L"The result is " << result << endl;
   }
   catch (operation_timed_out&)
   {
      wcout << L"The operation timed out." << endl;
   }
}

В этом примере формируются следующие данные:

The operation timed out.

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

[В начало]

Несколько исключений

Если задача или параллельный алгоритм получает несколько исключений, среда выполнения маршалирует только одно из этих исключений в контекст вызова. Среда выполнения не гарантирует, какое исключение он маршалирует.

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

// eh-multiple.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
   const int min = 0;
   const int max = 10;
   
   // Print values in a parallel_for loop. Use a try-catch block to 
   // handle any exceptions that occur in the loop.
   try
   {
      parallel_for(-5, 20, [min,max](int i)
      {
         // Throw an exeception if the input value is less than the 
         // minimum or greater than the maximum.

         // Otherwise, print the value to the console.

         if (i < min)
         {
            stringstream ss;
            ss << i << ": the value is less than the minimum.";
            throw exception(ss.str().c_str());
         }
         else if (i > max)
         {
            stringstream ss;
            ss << i << ": the value is greater than than the maximum.";
            throw exception(ss.str().c_str());
         }
         else
         {
            wstringstream ss;
            ss << i << endl;
            wcout << ss.str();
         }
      });
   }
   catch (exception& e)
   {
      // Print the error to the console.
      wcerr << L"Caught exception: " << e.what() << endl;
   }  
}

В следующем примере показаны примеры выходных данных для этого примера.

8293104567Caught exception: -5: the value is less than the minimum.

[В начало]

Отмена

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

[В начало]

Упрощенные задачи

Упрощенная задача — это задача, которую вы планируете непосредственно из объекта concurrency::Scheduler . Упрощенные задачи имеют меньше накладных расходов, чем обычные задачи. Однако среда выполнения не перехватывает исключения, создаваемые упрощенными задачами. Вместо этого исключение перехватываются необработанным обработчиком исключений, который по умолчанию завершает процесс. Поэтому используйте соответствующий механизм обработки ошибок в приложении. Дополнительные сведения о упрощенных задачах см. в разделе Планировщик задач.

[В начало]

Асинхронные агенты

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

В следующем примере показан один из способов обработки исключений в классе, наследуемом от параллелизма::agent. В этом примере определяется points_agent класс. Метод points_agent::run считывает point объекты из буфера сообщений и выводит их в консоль. Метод run создает исключение, если он получает NULL указатель.

Метод run окружает все работы в блоке try-catch . Блок catch сохраняет исключение в буфере сообщений. Приложение проверка, столкнувшись ли агент с ошибкой, прочитав из этого буфера после завершения агента.

// eh-agents.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace concurrency;
using namespace std;

// Defines a point with x and y coordinates.
struct point
{
   int X;
   int Y;
};

// Informs the agent to end processing.
point sentinel = {0,0};

// An agent that prints point objects to the console.
class point_agent : public agent
{
public:
   explicit point_agent(unbounded_buffer<point*>& points)
      : _points(points)
   { 
   }

   // Retrieves any exception that occurred in the agent.
   bool get_error(exception& e)
   {
      return try_receive(_error, e);
   }

protected:
   // Performs the work of the agent.
   void run()
   {
      // Perform processing in a try block.
      try
      {
         // Read from the buffer until we reach the sentinel value.
         while (true)
         {
            // Read a value from the message buffer.
            point* r = receive(_points);

            // In this example, it is an error to receive a 
            // NULL point pointer. In this case, throw an exception.
            if (r == NULL)
            {
               throw exception("point must not be NULL");
            }
            // Break from the loop if we receive the 
            // sentinel value.
            else if (r == &sentinel)
            {
               break;
            }
            // Otherwise, do something with the point.
            else
            {
               // Print the point to the console.
               wcout << L"X: " << r->X << L" Y: " << r->Y << endl;
            }
         }
      }
      // Store the error in the message buffer.
      catch (exception& e)
      {
         send(_error, e);
      }

      // Set the agent status to done.
      done();
   }

private:
   // A message buffer that receives point objects.
   unbounded_buffer<point*>& _points;

   // A message buffer that stores error information.
   single_assignment<exception> _error;
};

int wmain()
{  
   // Create a message buffer so that we can communicate with
   // the agent.
   unbounded_buffer<point*> buffer;

   // Create and start a point_agent object.
   point_agent a(buffer);
   a.start();

   // Send several points to the agent.
   point r1 = {10, 20};
   point r2 = {20, 30};
   point r3 = {30, 40};

   send(buffer, &r1);
   send(buffer, &r2);
   // To illustrate exception handling, send the NULL pointer to the agent.
   send(buffer, reinterpret_cast<point*>(NULL));
   send(buffer, &r3);
   send(buffer, &sentinel);

   // Wait for the agent to finish.
   agent::wait(&a);
  
   // Check whether the agent encountered an error.
   exception e;
   if (a.get_error(e))
   {
      cout << "error occurred in agent: " << e.what() << endl;
   }
   
   // Print out agent status.
   wcout << L"the status of the agent is: ";
   switch (a.status())
   {
   case agent_created:
      wcout << L"created";
      break;
   case agent_runnable:
      wcout << L"runnable";
      break;
   case agent_started:
      wcout << L"started";
      break;
   case agent_done:
      wcout << L"done";
      break;
   case agent_canceled:
      wcout << L"canceled";
      break;
   default:
      wcout << L"unknown";
      break;
   }
   wcout << endl;
}

В этом примере формируются следующие данные:

X: 10 Y: 20
X: 20 Y: 30
error occurred in agent: point must not be NULL
the status of the agent is: done

trycatch-Так как блок существует вне while цикла, агент завершает обработку при возникновении первой ошибки. trycatch-Если блок находится внутри while цикла, агент будет продолжаться после возникновения ошибки.

В этом примере сохраняются исключения в буфере сообщений, чтобы другой компонент может отслеживать агент ошибок при выполнении. В этом примере используется объект параллелизма::single_assignment для хранения ошибки. В случае, когда агент обрабатывает несколько исключений, класс сохраняет только первое сообщение, single_assignment переданное в него. Чтобы сохранить только последнее исключение, используйте класс параллелизма::overwrite_buffer . Чтобы сохранить все исключения, используйте класс параллелизма::unbounded_buffer . Дополнительные сведения об этих блоках сообщений см. в разделе "Асинхронные блоки сообщений".

Дополнительные сведения об асинхронных агентах см. в разделе "Асинхронные агенты".

[В начало]

Итоги

[В начало]

См. также

Среда выполнения с параллелизмом
Параллелизм задач
Параллельные алгоритмы
Отмена в библиотеке параллельных шаблонов
Планировщик заданий
Асинхронные агенты