Share via


Control de excepciones en el runtime de simultaneidad

El Runtime de simultaneidad usa el control de excepciones de C++ para comunicar muchas modalidades de errores. Entre estos errores, se incluyen el uso no válido del runtime, errores de runtime, como el que se produce al no poder adquirir un recurso, y errores que afectan a funciones de trabajo que el usuario proporciona a las tareas y los grupos de tareas. Cuando una tarea o un grupo de tareas produce una excepción, el runtime mantiene esa excepción y serializa las referencias en el contexto que espera a que finalice la tarea o el grupo de tareas. En el caso de componentes tales como tareas ligeras y agentes, el runtime no administra las excepciones en nombre del usuario. En estos casos, debe implementar su propio mecanismo del control de excepciones. En este tema se describe cómo controla el runtime las excepciones que producen las tareas, los grupos de tareas, las tareas ligeras y los agentes asincrónicos, y cómo se responde a las excepciones en las aplicaciones.

Puntos clave

  • Cuando una tarea o un grupo de tareas produce una excepción, el runtime mantiene esa excepción y serializa las referencias en el contexto que espera a que finalice la tarea o el grupo de tareas.

  • Si es posible, delimite cada llamada a concurrency::task::get y a concurrency::task::wait con un bloque try/catch para controlar los errores de los que es posible recuperarse. El runtime finaliza la aplicación si una tarea produce una excepción y la tarea, una de sus continuaciones o la aplicación principal no detecta la excepción.

  • Siempre se ejecuta una continuación basada en tareas, independientemente de si la tarea anterior se completó correctamente, produjo una excepción o se canceló. No se ejecuta una continuación basada en valores si se inicia la tarea anterior o se cancela.

  • Dado que las continuaciones basadas en tareas siempre se ejecutan, tal vez le interese agregar una continuación basada en tareas al final de la cadena de continuación. Puede contribuir a garantizar que el código tenga en cuenta todas las excepciones.

  • El entorno de ejecución produce concurrency::task_canceled cuando llama a concurrency::task::get y se cancela esa tarea.

  • El runtime no administra excepciones para los agentes y las tareas ligeras.

En este documento

Tareas y continuaciones

En esta sección se describe la manera en que el entorno de ejecución controla las excepciones que producen los objetos concurrency::task y sus continuaciones. Para más información sobre la tarea y el modelo de continuación, consulte Paralelismo de tareas.

Cuando se produce una excepción en el cuerpo de una función de trabajo que se pasa a un objeto task, el entorno de ejecución almacena esa excepción y la señaliza en el contexto que llama a concurrency::task::get o concurrency::task::wait. En el documento Paralelismo de tareas se describen las continuaciones basadas en tareas frente a las basadas en valores pero, en resumen, una continuación basada en valores toma un parámetro de tipo T y una continuación basada en tareas toma un parámetro de tipo task<T>. Si una tarea que se inicia tiene una o varias continuaciones basadas en valores, dichas continuaciones no se programan para su ejecución. En el siguiente ejemplo, se muestra este comportamiento:

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

Una continuación basada en tareas le permite administrar cualquier excepción producida por la tarea anterior. Una ejecución basada en tareas siempre se ejecuta, independientemente de si la tarea se completó correctamente, produjo una excepción o se canceló. Cuando una tarea produce una excepción, sus continuaciones basadas en tareas se programan para su ejecución. En el ejemplo siguiente se muestra una tarea que se inicia siempre. La tarea tiene dos continuaciones; una está basada en valores y la otra, en tareas. La excepción basada en tareas se ejecuta siempre y, por tanto, puede capturar la excepción que genera la tarea anterior. Cuando el ejemplo espera que terminen ambas continuaciones, la excepción se produce de nuevo porque la excepción de la tarea se produce siempre cuando se llama a task::get o 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.
*/

Se recomienda usar continuaciones basadas en tareas para capturar excepciones que pueda controlar. Dado que las continuaciones basadas en tareas siempre se ejecutan, tal vez le interese agregar una continuación basada en tareas al final de la cadena de continuación. Puede contribuir a garantizar que el código tenga en cuenta todas las excepciones. En el ejemplo siguiente se muestra una cadena de continuación básica basada en valores. Se inicia la tercera tarea de la cadena y, por tanto, no se ejecuta ninguna continuación basada en valores que la siga. Sin embargo, la continuación final está basada en tareas y, por tanto, siempre se ejecuta. Esta continuación final controla la excepción que produce la tercera tarea.

Se recomienda capturar las excepciones más específicas que pueda. Puede omitir esta continuación basada en tareas final si no tiene excepciones específicas para detectar. Cualquier excepción seguirá siendo no controlada y puede finalizar la aplicación.

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

Sugerencia

Puede usar el método concurrency::task_completion_event::set_exception para asociar una excepción a un evento de finalización de la tarea. En el documento Paralelismo de tareas se describe la clase concurrency::task_completion_event con mayor detalle.

concurrency::task_canceled es un tipo de excepción de tiempo de ejecución importante que está relacionado con task. El runtime produce task_canceled cuando llama a task::get y se cancela esa tarea. (Por el contrario, task::wait devuelve task_status::canceled y no produce un excepción). Puede detectar y controlar esta excepción desde una continuación basada en tareas o al llamar a task::get. Para más información sobre la cancelación de tareas, consulte Cancelación en la biblioteca PPL.

Precaución

Nunca inicie task_canceled desde su código. Llame a concurrency::cancel_current_task en su lugar.

El runtime finaliza la aplicación si una tarea produce una excepción y la tarea, una de sus continuaciones o la aplicación principal no detecta la excepción. Si se bloquea la aplicación, puede configurar Visual Studio para que se interrumpa cuando se produzcan excepciones de C++. Tras diagnosticar la ubicación de la excepción no controlada, utilice una continuación basada en tareas para controlarla.

En la sección Excepciones producidas por el Runtime de este documento se describe con mayor detalle cómo trabajar con excepciones en tiempo de ejecución.

[Arriba]

Grupos de tareas y algoritmos paralelos

En esta sección se describe cómo controla el runtime las excepciones que producen los grupos de tareas. Esta sección también se aplica a algoritmos paralelos, como concurrency::parallel_for, puesto que estos algoritmos se basan en grupos de tareas.

Precaución

Asegúrese de que entiende los efectos que tienen las excepciones sobre las tareas dependientes. Para información sobre prácticas recomendadas acerca del uso del control de excepciones con tareas o algoritmos paralelos, consulte la sección Comprender cómo afectan la cancelación y el control de excepciones a la destrucción de objetos del tema Procedimientos recomendados en la biblioteca de patrones paralelos.

Para obtener más información sobre los grupos de tareas, vea Paralelismo de tareas. Para obtener más información sobre los algoritmos en paralelo, vea Algoritmos paralelos.

Cuando se produce una excepción en el cuerpo de una función de trabajo que se pasa a un objeto concurrency::task_group o concurrency::structured_task_group, el entorno de ejecución almacena esa excepción y la señaliza en el contexto que llama a concurrency::task_group::wait, concurrency::structured_task_group::wait, concurrency::task_group::run_and_wait o concurrency::structured_task_group::run_and_wait. El runtime también detiene todas las tareas activas que están en el grupo de tareas (incluidas las que se encuentran en grupos de tareas secundarios) y descarta cualquier tarea que no se haya iniciado todavía.

En el siguiente ejemplo se muestra la estructura básica de una función de trabajo que produce una excepción. En el ejemplo se usa un objeto task_group para imprimir los valores de dos objetos point en paralelo. La función de trabajo print_point imprime los valores de un objeto point en la consola. La función de trabajo produce una excepción si el valor de entrada es NULL. El runtime almacena esta excepción y calcula las referencias en el contexto que llama a 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 ejemplo produce el siguiente resultado:

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

Para ver un ejemplo completo que usa el control de excepciones en un grupo de tareas, consulte Cómo usar el control de excepciones para interrumpir un bucle Parallel.

[Arriba]

Excepciones producidas por el runtime

Una excepción puede ser el resultado de una llamada al runtime. La mayoría de los tipos de excepción, salvo concurrency::task_canceled y concurrency::operation_timed_out, indican un error de programación. Normalmente, estos errores son irrecuperables y, por consiguiente, no se deben detectar ni administrar en el código de aplicación. Sugerimos sólo detectar o controlar los errores irrecuperables en el código de aplicación si es necesario diagnosticar errores de programación. Sin embargo, conocer los tipos de excepciones que define el runtime puede ayudar a diagnosticar los errores de programación.

El mecanismo de control de excepciones es el mismo para las excepciones que produce el runtime y las excepciones que producen las funciones de trabajo. Por ejemplo, la función concurrency::receive produce la excepción operation_timed_out cuando no recibe un mensaje en el período de tiempo especificado. Si receive produce una excepción en una función de trabajo que se pasa a un grupo de tareas, el runtime almacena esa excepción y serializa las referencias en el contexto que llama a task_group::wait, structured_task_group::wait, task_group::run_and_wait o structured_task_group::run_and_wait.

En el ejemplo siguiente se usa el algoritmo concurrency::parallel_invoke para ejecutar dos tareas en paralelo. La primera tarea espera cinco segundos y, a continuación, envía un mensaje a un búfer de mensajes. La segunda tarea usa la función receive para esperar tres segundos a recibir un mensaje desde el mismo búfer de mensajes. La función receive produce la excepción operation_timed_out si no recibe el mensaje en dicho período de tiempo.

// 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 ejemplo produce el siguiente resultado:

The operation timed out.

Para evitar la terminación anómala de la aplicación, asegúrese de que el código controla las excepciones cuando llama al runtime. Controle también las excepciones al llamar a código externo que use el Runtime de simultaneidad, por ejemplo, una biblioteca de otro fabricante.

[Arriba]

Varias excepciones

Si una tarea o algoritmo paralelo recibe varias excepciones, el runtime serializa las referencias sólo de una de esas excepciones en el contexto de la llamada. El runtime no garantiza para qué excepción se calculan las referencias.

En el ejemplo siguiente se usa el algoritmo parallel_for para imprimir números en la consola. Produce una excepción si el valor de entrada es menor que un determinado valor mínimo o mayor que un determinado valor máximo. En este ejemplo, varias funciones de trabajo pueden producir una excepción.

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

A continuación, se muestra la salida de este ejemplo.

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

[Arriba]

Cancelación

No todas las excepciones indican un error. Por ejemplo, un algoritmo de búsqueda podría utilizar el control de excepciones para detener su tarea asociada cuando encuentra el resultado. Para más información sobre el uso de los mecanismos de cancelación en el código, consulte Cancelación en la biblioteca PPL.

[Arriba]

Tareas ligeras

Una tarea ligera es aquella que se programa directamente desde un objeto concurrency::Scheduler. Las tareas ligeras implican una menor sobrecarga que las tareas ordinarias. Sin embargo, el runtime no detecta las excepciones producidas por las tareas ligeras. En su lugar, el controlador de excepciones no controladas, que de forma predeterminada finaliza el proceso, detecta la excepción. Por consiguiente, use un mecanismo de control de errores adecuado en su aplicación. Para obtener más información sobre las tareas ligeras, consulte Programador de tareas.

[Arriba]

Agentes asincrónicos

Como sucede con las tareas ligeras, el runtime no controla las excepciones producidas por agentes asincrónicos.

En el ejemplo siguiente se muestra una manera de controlar excepciones en una clase que se deriva de concurrency::agent. En este ejemplo se define la clase points_agent. El método points_agent::run lee los objetos point del búfer de mensajes y los imprime en la consola. El método run produce una excepción si recibe un puntero NULL.

El método run rodea todo el trabajo en un bloque try-catch. El bloque catch almacena la excepción en un búfer de mensajes. La aplicación comprueba si el agente encontró un error leyendo el búfer cuando el agente finaliza.

// 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 ejemplo produce el siguiente resultado:

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

Dado que el bloque try-catch existe fuera del bucle while, el agente finaliza el procesamiento cuando encuentra el primer error. Si el bloque try-catch estuviese dentro del bucle while, el agente continuaría después de producirse el error.

En este ejemplo se almacenan las excepciones en un búfer de mensajes para que otro componente pueda supervisar los errores del agente mientras se ejecuta. En este ejemplo se usa un objeto concurrency::single_assignment para almacenar el error. Cuando un agente controla varias excepciones, la clase single_assignment almacena únicamente el primer mensaje que se le pasa. Para almacenar solo la última excepción, use la clase concurrency::overwrite_buffer. Para almacenar todas las excepciones, use la clase concurrency::unbounded_buffer. Para más información sobre estos bloques de mensajes, consulte Bloques de mensajes asincrónicos.

Para más información sobre los agentes asincrónicos, consulte Agentes asincrónicos.

[Arriba]

Resumen

[Arriba]

Consulte también

Runtime de simultaneidad
Paralelismo de tareas
Algoritmos paralelos
Cancelación en la biblioteca PPL
Programador de tareas
Agentes asincrónicos