Share via


Tratamento de exceções no runtime de simultaneidade

O Runtime de Simultaneidade usa o tratamento de exceções do C++ para comunicar muitos tipos de erros. Esses erros incluem o uso inválido do runtime, erros de runtime, como falha na aquisição de um recurso e erros que ocorrem em funções de trabalho que você fornece a tarefas e grupos de tarefas. Quando uma tarefa ou grupo de tarefas gera uma exceção, o runtime mantém essa exceção e realiza marshaling dela no contexto que aguarda a conclusão da tarefa ou do grupo de tarefas. Para componentes como tarefas leves e agentes, o runtime não gerencia exceções para você. Nesses casos, você precisa implementar um mecanismo próprio de tratamento de exceções. Este tópico descreve como o runtime lida com exceções geradas por tarefas, grupos de tarefas, tarefas leves e agentes assíncronos e como responder a exceções em seus aplicativos.

Pontos Principais

  • Quando uma tarefa ou grupo de tarefas gera uma exceção, o runtime mantém essa exceção e realiza marshaling dela no contexto que aguarda a conclusão da tarefa ou do grupo de tarefas.

  • Quando possível, cerque todas as chamadas para concurrency::task::get e concurrency::task::wait com um bloco try/catch para lidar com erros dos quais você pode se recuperar. O runtime encerrará o aplicativo se uma tarefa gerar uma exceção e essa exceção não for capturada pela tarefa, uma das continuações dela ou pelo aplicativo principal.

  • Uma continuação baseada em tarefa sempre é executada; não importa se a tarefa antecedente foi concluída com êxito, gerou uma exceção ou foi cancelada. Uma continuação baseada em valor não será executada se a tarefa antecedente gerar uma exceção ou for cancelada.

  • Como as continuações baseadas em tarefa sempre são executadas, considere se uma continuação baseada em tarefa deve ser adicionada ao final da sua cadeia de continuação. Isso pode ajudar a garantir que seu código observe todas as exceções.

  • O runtime gera concurrency::task_canceled quando você chama concurrency::task::get e essa tarefa é cancelada.

  • O runtime não gerencia exceções para tarefas leves e agentes.

Neste Documento

Tarefas e Continuações

Esta seção descreve como o runtime manupula exceções geradas por objetos concurrency::task e as respectivas continuações. Para obter mais informações sobre o modelo de tarefa e continuação, confira Paralelismo da Tarefa.

Quando você gera uma exceção no corpo de uma função de trabalho que você passa para um objeto task, o runtime armazena essa exceção e a coloca em marshaling no contexto que chama concurrency::task::get ou concurrency::task::wait. O documento Task Parallelism descreve as continuações baseadas em tarefa versus baseadas em valor, mas, para resumir, uma continuação baseada em valor usa um parâmetro de tipo T e uma continuação baseada em tarefa usa um parâmetro de tipo task<T>. Se uma tarefa que gere exceções tiver uma ou mais continuações baseadas em valor, essas continuações não serão agendadas para execução. O seguinte exemplo a seguir ilustra esse comportamento:

// 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.
*/

Uma continuação baseada em tarefa permite que você manipule qualquer exceção gerada pela tarefa antecedente. Uma continuação baseada em tarefa sempre é executada; não importa se a tarefa foi concluída com êxito, gerou uma exceção ou foi cancelada. Quando uma tarefa gera uma exceção, as continuações dela baseadas em tarefa são agendadas para serem executadas. O exemplo a seguir mostra uma tarefa que sempre gera exceções. A tarefa tem duas continuações; uma é baseada em valor e a outra é baseado em tarefas. A exceção baseada em tarefa sempre é executada e, portanto, pode capturar a exceção gerada pela tarefa anterior. Quando o exemplo aguarda a conclusão de ambas as continuações, a exceção é gerada novamente porque a exceção de tarefa sempre é gerada quando task::get ou task::wait é chamada.

// 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.
*/

Recomendamos que você use continuações baseadas em tarefa para capturar exceções que você pode manipular. Como as continuações baseadas em tarefa sempre são executadas, considere se uma continuação baseada em tarefa deve ser adicionada ao final da sua cadeia de continuação. Isso pode ajudar a garantir que seu código observe todas as exceções. O exemplo a seguir mostra uma cadeia de continuação básica baseada em valor. A terceira tarefa na cadeia é gera exceções e, portanto, as continuações baseadas em valor que a seguem não são executadas. No entanto, a continuação final é baseada em tarefa e, portanto, sempre é executada. Essa continuação final manipula a exceção gerada pela terceira tarefa.

Recomendamos que você capture as exceções mais específicas que puder. Você pode omitir essa continuação final baseada em tarefas se não tiver exceções específicas para capturar. Qualquer exceção permanecerá sem tratamento e poderá encerrar o aplicativo.

// 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>
*/

Dica

Você pode usar o método concurrency::task_completion_event::set_exception para associar uma exceção a um evento de conclusão de tarefa. O documento Task Parallelism descreve a classe concurrency::task_completion_event com mais detalhes.

concurrency::task_canceled é um tipo de exceção de runtime importante relacionado a task. O runtime gera task_canceled quando você chama task::get e essa tarefa é cancelada. (Por outro lado, task::wait retorna task_status::canceled e não gera exceções.) Você pode capturar e lidar com essa exceção de uma continuação baseada em tarefas ou quando você chama task::get. Para saber mais sobre cancelamento de tarefas, confira Cancelamento no PPL.

Cuidado

Nunca gere task_canceled do seu código. Em vez disso, chame concurrency::cancel_current_task.

O runtime encerrará o aplicativo se uma tarefa gerar uma exceção e essa exceção não for capturada pela tarefa, uma das continuações dela ou pelo aplicativo principal. Se o aplicativo falhar, você poderá configurar o Visual Studio para interromper quando exceções C++ forem geradas. Depois de diagnosticar o local da exceção sem tratamento, use uma continuação baseada em tarefa para lidar com ela.

A seção Exceções geradas pelo runtime neste documento descreve com mais detalhes como trabalhar com exceções de runtime.

[Parte superior]

Grupos de tarefas e algoritmos paralelos

Esta seção descreve como o runtime lida com exceções geradas por grupos de tarefas. Esta seção também se aplica a algoritmos paralelos, como concurrency::p arallel_for, porque esses algoritmos se baseiam em grupos de tarefas.

Cuidado

Certifique-se de entender os efeitos que as exceções têm em tarefas dependentes. Para obter práticas recomendadas sobre como usar o tratamento de exceções com tarefas ou algoritmos paralelos, confira a seção Entender como o cancelamento e o tratamento de exceções afetam a destruição de objetos nas práticas recomendadas no tópico da Biblioteca de Padrões Paralelos.

Para obter mais informações sobre grupos de tarefas, confira Paralelismo de Tarefa. Para obter mais informações sobre algoritmos paralelos, consulte Algoritmos paralelos.

Quando você gera uma exceção no corpo de uma função de trabalho que você passa para um objeto concurrency::task_group ou concurrency::structured_task_group, o runtime armazena essa exceção e a faz marshaling dela para o contexto que chama concurrency::task_group::wait, concurrency::structured_task_group::wait, concurrency::task_group::run_and_wait ou concurrency::structured_task_group::run_and_wait. O runtime também interrompe todas as tarefas ativas que estão no grupo de tarefas, incluindo aquelas em grupos de tarefas filho, e descarta todas as tarefas que ainda não foram iniciadas.

O exemplo a seguir mostra a estrutura básica de uma função de trabalho que gera uma exceção. O exemplo usa um objeto task_group para imprimir os valores de dois objetos point em paralelo. A função de trabalho print_point imprime os valores de um objeto point no console. A função de trabalho gerará uma exceção se o valor de entrada for NULL. O runtime armazena essa exceção e a faz marshaling para o contexto que chama 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;
   }
}

Este exemplo gerencia a seguinte saída.

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

Para obter um exemplo completo que usa o tratamento de exceções em um grupo de tarefas, confira Como usar o tratamento de exceções para interromper de um loop paralelo.

[Parte superior]

Exceções lançadas pelo tempo de execução

Uma exceção pode resultar de uma chamada para o runtime. A maioria dos tipos de exceção, exceto por concurrency::task_canceled e concurrency::operation_timed_out, indicam um erro de programação. Normalmente, esses erros são irrecuperáveis e, portanto, não devem ser capturados ou tratados pelo código do aplicativo. Sugerimos que você só capture ou manipule erros irrecuperáveis no código do aplicativo quando precisar diagnosticar erros de programação. No entanto, entender os tipos de exceção definidos pelo runtime pode ajudar você a diagnosticar erros de programação.

O mecanismo de tratamento de exceções é o mesmo para exceções geradas pelo runtime e exceções geradas por funções de trabalho. Por exemplo, a função concurrency::receive é gerada operation_timed_out quando não recebe uma mensagem no período de tempo especificado. Se receive gerar uma exceção em uma função de trabalho que você passar para um grupo de tarefas, o runtime armazenará essa exceção e realizará marshaling dela para o contexto, que chamará task_group::wait, structured_task_group::wait, task_group::run_and_wait ou structured_task_group::run_and_wait.

O exemplo a seguir usa o algoritmo concurrency::p arallel_invoke para executar duas tarefas em paralelo. A primeira tarefa aguarda cinco segundos e envia uma mensagem para um buffer de mensagem. A segunda tarefa usa a função receive para aguardar três segundos para receber uma mensagem do mesmo buffer de mensagem. A função receive gerará operation_timed_out se não receber a mensagem no período de tempo.

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

Este exemplo gerencia a seguinte saída.

The operation timed out.

Para evitar o encerramento anormal do aplicativo, verifique se o código trata exceções quando ele chama o runtime. Também trate exceções quando você chama o código externo que usa o Runtime de Simultaneidade, por exemplo, uma biblioteca de terceiros.

[Parte superior]

Várias exceções

Se uma tarefa ou algoritmo paralelo receber várias exceções, o runtime aplicará apenas uma dessas exceções ao contexto de chamada. O runtime não garante de qual exceção ele realiza marshaling.

O exemplo a seguir usa o algoritmo parallel_for para imprimir números no console. Ele gera uma exceção se o valor de entrada for menor que algum valor mínimo ou maior que algum valor máximo. Neste exemplo, várias funções de trabalho podem gerar uma exceção.

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

O exemplo a seguir mostra a saída de exemplo para este exemplo.

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

[Parte superior]

Cancelamento

Nem todas as exceções indicam um erro. Por exemplo, um algoritmo de pesquisa poderá usar o tratamento de exceção para interromper a tarefa associada a ele quando encontrar o resultado. Para obter mais informações sobre como usar mecanismos de cancelamento em seu código, confira Cancelamento no PPL.

[Parte superior]

Tarefas Leves

Uma tarefa leve é uma tarefa que você agenda diretamente de um objeto concurrency::Scheduler. Tarefas leves têm menos sobrecarga do que tarefas comuns. No entanto, o runtime não captura exceções geradas por tarefas leves. Em vez disso, a exceção é capturada pelo manipulador de exceção sem tratamento, que, por padrão, encerra o processo. Portanto, use um mecanismo apropriado de tratamento de erros em seu aplicativo. Para obter mais informações sobre tarefas leves, confira Agendador de Tarefas.

[Parte superior]

Agentes Assíncronos

Como tarefas leves, o runtime não gerencia exceções geradas por agentes assíncronos.

O exemplo a seguir mostra uma maneira de lidar com exceções em uma classe derivada de concurrency::agent. Este exemplo define a classe points_agent. O método points_agent::run lê objetos point do buffer de mensagem e os imprime no console. O método run gerará uma exceção se receber um ponteiro NULL.

O método run envolve todo o trabalho em um bloco try-catch. O bloco catch armazena a exceção em um buffer de mensagem. O aplicativo verifica se o agente encontrou um erro lendo desse buffer após a conclusão do agente.

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

Este exemplo gerencia a seguinte saída.

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

Como o bloco try-catch existe fora do loop while, o agente termina o processamento quando encontra o primeiro erro. Se o bloco try-catch estivesse dentro do loop while, o agente continuaria após a ocorrência de um erro.

Este exemplo armazena exceções em um buffer de mensagem para que outro componente possa monitorar o agente em busca de erros conforme ele é executado. Este exemplo usa um objeto concurrency::single_assignment para armazenar o erro. No caso em que um agente lida com várias exceções, a classe single_assignment armazena apenas a primeira mensagem que é passada para ele. Para armazenar apenas a última exceção, use a classe concurrency::overwrite_buffer. Para armazenar todas as exceções, use a classe concurrency::unbounded_buffer. Para obter mais informações sobre esses blocos de mensagens, confira Blocos de Mensagens Assíncronas.

Para mais informações sobre agentes assíncronos, confira Agentes Assíncronos.

[Parte superior]

Resumo

[Parte superior]

Confira também

Runtime de Simultaneidade
Paralelismo de tarefas
Algoritmos paralelos
Cancelamento no PPL
Agendador de Tarefas
Agentes assíncronos