Cancelación en la biblioteca PPL

En este tema se explica el rol de cancelación de la Biblioteca de modelos de procesamiento paralelo (PPL), cómo se cancela el trabajo paralelo y cómo se determina cuándo un grupo de tareas está cancelado.

Secciones

  • Árboles de trabajo paralelo

  • Cancelar tareas paralelas

  • Cancelar algoritmos paralelos

  • Cuándo no conviene usar la cancelación

Árboles de trabajo paralelo

PPL usa grupos de tareas para administrar tareas y cálculos específicos. Los grupos de tareas se pueden anidar para formar árboles de trabajo paralelo. En la ilustración siguiente se muestra un árbol de trabajo paralelo. En esta ilustración, tg1 y tg2 representan grupos de tareas; t1, t2, t3, t4 y t5 representan tareas.

Árbol de trabajo paralelo

En el ejemplo siguiente se muestra el código necesario para crear el árbol de la ilustración. En este ejemplo, tg1 y tg2 son objetos Concurrency::structured_task_group; t1, t2, t3, t4 y t5 son objetos Concurrency::task_handle.

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

using namespace Concurrency;
using namespace std;

void create_task_tree()
{   
   // Create a task group that serves as the root of the tree.
   structured_task_group tg1;

   // Create a task that contains a nested task group.
   auto t1 = make_task([&] {
      structured_task_group tg2;

      // Create a child task.
      auto t4 = make_task([&] {
         // TODO: Perform work here.
      });

      // Create a child task.
      auto t5 = make_task([&] {
         // TODO: Perform work here.
      });

      // Run the child tasks and wait for them to finish.
      tg2.run(t4);
      tg2.run(t5);
      tg2.wait();
   });

   // Create a child task.
   auto t2 = make_task([&] {
      // TODO: Perform work here.
   });

   // Create a child task.
   auto t3 = make_task([&] {
      // TODO: Perform work here.
   });

   // Run the child tasks and wait for them to finish.
   tg1.run(t1);
   tg1.run(t2);
   tg1.run(t3);
   tg1.wait();   
}

[Ir al principio]

Cancelar tareas paralelas

Existen dos mecanismos para cancelar el trabajo paralelo. Uno de ellos consiste en llamar al método Concurrency::task_group::cancel o al método Concurrency::structured_task_group::cancel. El otro consiste en iniciar una excepción en el cuerpo de una función de trabajo de una tarea.

El método cancel es más eficaz que el control de excepciones para cancelar un árbol de trabajo paralelo. El método cancel cancela un grupo de tareas y los grupos de tareas secundarios de forma descendente. Por el contrario, el control de excepciones funciona de manera ascendente y debe cancelar cada grupo de tareas secundario por separado a medida que la excepción se propaga hacia arriba.

En las secciones siguientes se muestra cómo se usa el método cancel y el control de excepciones para cancelar el trabajo paralelo. Para obtener más ejemplos en los que se cancelan tareas paralelas, vea Cómo: Usar la cancelación para interrumpir un bucle Parallel y Cómo: Usar el control de excepciones para interrumpir un bucle Parallel.

Usar el método cancel para cancelar el trabajo paralelo

Los métodos Concurrency::task_group::cancel y Concurrency::structured_task_group::cancel establecen un grupo de tareas en el estado cancelado.

Nota

El runtime usa el control de excepciones para implementar la cancelación. No debe detectar ni administrar estas excepciones en su código. Además, le recomendamos que escriba código seguro ante excepciones en los cuerpos de las funciones de las tareas. Por ejemplo, puede usar el modelo Resource Acquisition Is Initialization (RAII) para asegurarse de que los recursos se administran correctamente cuando se inicia una excepción en el cuerpo de una tarea. Si desea consultar un ejemplo completo en el que se usa el modelo RAII para limpiar un recurso en una tarea cancelable, vea Tutorial: Quitar trabajo de un subproceso de la interfaz de usuario.

Después de llamar a cancel, el grupo de tareas no iniciará ninguna otra tarea posterior. Los métodos cancel pueden invocarse a través de varias tareas secundarias. Una tarea cancelada hace que los métodos Concurrency::task_group::wait y Concurrency::structured_task_group::wait devuelvan Concurrency::canceled.

El método cancel solo afecta a las tareas secundarias. Por ejemplo, si se cancela el grupo de tareas tg1 de la ilustración del árbol de trabajo paralelo, todas las tareas del árbol (t1, t2, t3, t4 y t5) se verán afectadas. Si se cancela el grupo de tareas anidado tg2, solo las tareas t4 y t5 se verán afectadas.

Al llamar al método cancel, todos los grupos de tareas secundarios también se cancelan. Sin embargo, la cancelación no afecta a ningún elemento primario del grupo de tareas de un árbol de trabajo paralelo. En los ejemplos siguientes se usa el árbol de trabajo paralelo de la ilustración para mostrar este comportamiento.

En el primero de estos ejemplos se crea una función de trabajo para la tarea t4, que es un elemento secundario del grupo de tareas tg2. La función de trabajo llama a la función work en un bucle. Si las llamadas a work no se realizan correctamente, la tarea cancela su grupo de tareas primario. Esto hace que el grupo de tareas tg2 adopte el estado cancelado, pero no cancela el grupo de tareas tg1.

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }         
});

Este segundo ejemplo se parece el primero, salvo por el hecho de que la tarea cancela el grupo de tareas tg1. Esto afecta a todas las tareas del árbol (t1, t2, t3, t4y t5).

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel all tasks in the tree.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg1.cancel();
         break;
      }
   }   
});

La clase structured_task_group no es segura para la ejecución de subprocesos. Por tanto, si una tarea secundaria llama a un método de su objeto structured_task_group primario, se produce un comportamiento no especificado. Los métodos structured_task_group::cancel y Concurrency::structured_task_group::is_canceling son excepciones a esta regla. Una tarea secundaria puede llamar a estos métodos para cancelar la tarea primaria y comprobar la cancelación.

Usar excepciones para cancelar el trabajo paralelo

En el tema Control de excepciones en el runtime de simultaneidad se explica cómo Runtime de simultaneidad usa las excepciones para notificar errores. Sin embargo, no todas las excepciones indican un error. Por ejemplo, un algoritmo de búsqueda puede cancelar su grupo de tareas asociado cuando encuentra el resultado. Sin embargo, tal y como se mencionó anteriormente, el control de excepciones resulta menos eficaz que el método cancel para cancelar el trabajo paralelo.

Cuando se produce una excepción en el cuerpo de una función de trabajo que se pasa a un grupo de tareas, el runtime almacena esa excepción y calcula las referencias de la excepción en el contexto en el que se espera a que el grupo de tareas finalice. Como sucede con el método cancel, el runtime descarta cualquier tarea que no se haya iniciado todavía y no acepta nuevas tareas.

Este tercer ejemplo se parece al segundo, salvo en que la tarea t4 produce una excepción para cancelar el grupo de tareas tg2. En este ejemplo se usa un bloque try-catch para comprobar la cancelación cuando el grupo de tareas tg2 espera a que sus tareas secundarias finalicen. Al igual que en el primer ejemplo, esto hace que el grupo de tareas tg2 pase a tener el estado cancelado, pero no cancela el grupo de tareas tg1.

structured_task_group tg2;

// Create a child task.      
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, throw an exception to 
      // cancel the parent task.
      bool succeeded = work(i);
      if (!succeeded)
      {
         throw exception("The task failed");
      }
   }         
});

// Create a child task.
auto t5 = make_task([&] {
   // TODO: Perform work here.
});

// Run the child tasks.
tg2.run(t4);
tg2.run(t5);

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg2.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

En este cuarto ejemplo se usa el control de excepciones para cancelar todo el árbol de trabajo. En el ejemplo, la excepción se detecta cuando el grupo de tareas tg1 espera a que sus tareas secundarias finalicen y no cuando el grupo de tareas tg2 espera a sus tareas secundarias. Al igual que en el segundo ejemplo, esto hace que los dos grupos de tareas del árbol, tg1 y tg2, pasen a tener el estado cancelado.

// Run the child tasks.
tg1.run(t1);
tg1.run(t2);
tg1.run(t3);   

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg1.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

Como los métodos task_group::wait y structured_task_group::wait se inician cuando una tarea secundaria produce una excepción, no devuelven ningún valor.

Determinar cuándo el trabajo está cancelado

La cancelación es cooperativa. Por tanto, no se produce de forma inmediata. Si un grupo de tareas está cancelado, las llamadas de cada una de las tareas secundarias al runtime pueden activar un punto de interrupción, lo que hace que el runtime inicie y detecte un tipo de excepción interna para cancelar las tareas activas. El Runtime de simultaneidad no define puntos de interrupción concretos; estos pueden producirse en cualquier llamada al runtime. El runtime debe controlar las excepciones que se producen para poder llevar a cabo la cancelación. Por tanto, no deben controlarse excepciones desconocidas en el cuerpo de una tarea.

Si una tarea secundaria realiza una operación que exige mucho tiempo y no llama al runtime, debe comprobarse periódicamente si se he cancelado y si ha salido de forma puntual. En el ejemplo siguiente se muestra un mecanismo para determinar cuándo un trabajo está cancelado. La tarea t4 cancela el grupo de tareas primario cuando encuentra un error. La tarea t5 llama de tanto en tanto al método structured_task_group::is_canceling para comprobar si se ha cancelado. Si el grupo de tareas primario está cancelado, la tarea t5 imprime un mensaje y sale.

structured_task_group tg2;

// Create a child task.
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }
});

// Create a child task.
auto t5 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // To reduce overhead, occasionally check for 
      // cancelation.
      if ((i%100) == 0)
      {
         if (tg2.is_canceling())
         {
            wcout << L"The task was canceled." << endl;
            break;
         }
      }

      // TODO: Perform work here.
   }
});

// Run the child tasks and wait for them to finish.
tg2.run(t4);
tg2.run(t5);
tg2.wait();

En este ejemplo se comprueba la cancelación cada vez que se producen 100 iteraciones del bucle de la tarea. La frecuencia con la que se comprueba la cancelación depende de la cantidad de trabajo que realiza la tarea y la rapidez necesaria con la que las tareas deben responder a la cancelación.

Si no tiene acceso al objeto del grupo de tareas primario, llame a la función Concurrency::is_current_task_group_canceling para determinar si el grupo de tareas primario se ha cancelado.

[Ir al principio]

Cancelar algoritmos paralelos

Los algoritmos paralelos de PPL, por ejemplo, Concurrency::parallel_for, se basan en grupos de tareas. Por tanto, pueden usarse muchas técnicas similares para cancelar un algoritmo paralelo.

En los ejemplos siguientes se muestran varios mecanismos para cancelar un algoritmo paralelo.

En el ejemplo siguiente se usa el método Concurrency::structured_task_group::run_and_wait para llamar al algoritmo parallel_for. El método structured_task_group::run_and_wait espera a que la tarea proporcionada finalice. El objeto structured_task_group permite que la función de trabajo cancele la tarea.

// To enable cancelation, call parallel_for in a task group.
structured_task_group tg;

task_group_status status = tg.run_and_wait([&] {
   parallel_for(0, 100, [&](int i) {
      // Cancel the task when i is 50.
      if (i == 50)
      {
         tg.cancel();
      }
      else
      {
         // TODO: Perform work here.
      }
   });
});

// Print the task group status.
wcout << L"The task group status is: ";
switch (status)
{
case not_complete:
   wcout << L"not complete." << endl;
   break;
case completed:
   wcout << L"completed." << endl;
   break;
case canceled:
   wcout << L"canceled." << endl;
   break;
default:
   wcout << L"unknown." << endl;
   break;
}

Este ejemplo produce el siguiente resultado.

The task group status is: canceled.

En el siguiente ejemplo se usa el control de excepciones para cancelar un bucle parallel_for. El runtime calcula las referencias de la excepción en el contexto de la llamada.

try
{
   parallel_for(0, 100, [&](int i) {
      // Throw an exception to cancel the task when i is 50.
      if (i == 50)
      {
         throw i;
      }
      else
      {
         // TODO: Perform work here.
      }
   });
}
catch (int n)
{
   wcout << L"Caught " << n << endl;
}

Este ejemplo produce el siguiente resultado.

Caught 50

En el siguiente ejemplo se usa una marca booleana para coordinar la cancelación de un bucle parallel_for. En este ejemplo se ejecutan todas las tareas, ya que no se usa el método cancel ni el control de excepciones para cancelar el conjunto completo de tareas. Por tanto, esta técnica puede tener mayor sobrecarga computacional que un mecanismo de cancelación.

// Create a Boolean flag to coordinate cancelation.
bool canceled = false;

parallel_for(0, 100, [&](int i) {
   // For illustration, set the flag to cancel the task when i is 50.
   if (i == 50)
   {
      canceled = true;
   }

   // Perform work if the task is not canceled.
   if (!canceled)
   {
      // TODO: Perform work here.
   }
});

Cada uno de los métodos de cancelación tiene ventajas sobre los otros. Elija el método que mejor se ajuste a sus necesidades concretas.

[Ir al principio]

Cuándo no conviene usar la cancelación

El uso de la cancelación es adecuado cuando cada miembro de un grupo de tareas relacionadas puede salir de forma puntual. Sin embargo, hay algunos escenarios en los que la cancelación podría no resultar adecuada para su aplicación. Por ejemplo, dado que la cancelación de tareas es cooperativa, el conjunto completo de tareas no se cancelará si alguna tarea individual está bloqueada. Por ejemplo, si una tarea no se ha iniciado todavía pero desbloquea otra tarea activa, no se iniciará si el grupo de tareas está cancelado. Esto puede generar una situación de interbloqueo en la aplicación. Un segundo ejemplo donde el uso de la cancelación puede no ser adecuado es cuando se cancela una tarea, pero su tarea secundaria realiza una operación importante, como liberar un recurso. Dado que el conjunto completo de tareas se cancela a la vez que la tarea primaria, esa operación no se ejecutará. Para obtener un ejemplo que muestre este punto, vea la sección Comprender cómo afectan la cancelación y el control de excepciones a la destrucción de objetos en las mejores prácticas del tema Biblioteca de modelos de procesamiento paralelo.

[Ir al principio]

Temas relacionados

Referencia

task_group (Clase)

structured_task_group (Clase)

parallel_for (Función)

Historial de cambios

Fecha

Historial

Motivo

Marzo de 2011

Se agregó otro caso a la sección Cuándo no conviene usar la cancelación.

Mejora de la información.