Cancellation in the PPL

This topic explains the role of cancellation in the Parallel Patterns Library (PPL), how to cancel parallel work, and how to determine when a task group is canceled.

Sections

  • Parallel Work Trees

  • Canceling Parallel Tasks

  • Canceling Parallel Algorithms

  • When Not to Use Cancellation

Parallel Work Trees

The PPL uses task groups to manage fine-grained tasks and computations. You can nest task groups to form trees of parallel work. The following illustration shows a parallel work tree. In this illustration, tg1 and tg2 represent task groups; t1, t2, t3, t4, and t5 represent tasks.

A parallel work tree

The following example shows the code that is required to create the tree in the illustration. In this example, tg1 and tg2 are Concurrency::structured_task_group objects; t1, t2, t3, t4, and t5 are Concurrency::task_handle objects.

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

[go to top]

Canceling Parallel Tasks

There are two ways to cancel parallel work. One way is to call the Concurrency::task_group::cancel method or the Concurrency::structured_task_group::cancel method. The other way is to throw an exception in the body of a task work function.

The cancel method is more efficient than exception handling at canceling a parallel work tree. The cancel method cancels a task group and any child task groups in a top-down manner. Conversely, exception handling works in a bottom-up manner and must cancel each child task group independently as the exception propagates upward.

The following sections show how to use the cancel method and exception handling to cancel parallel work. For more examples that cancel parallel tasks, see How to: Use Cancellation to Break from a Parallel Loop and How to: Use Exception Handling to Break from a Parallel Loop.

Using the cancel Method to Cancel Parallel Work

The Concurrency::task_group::cancel and Concurrency::structured_task_group::cancel methods set a task group to the canceled state.

Note

The runtime uses exception handling to implement cancellation. Do not catch or handle these exceptions in your code. In addition, we recommend that you write exception-safe code in the function bodies for your tasks. For instance, you can use the Resource Acquisition Is Initialization (RAII) pattern to ensure that resources are correctly handled when an exception is thrown in the body of a task. For a complete example that uses the RAII pattern to clean up a resource in a cancelable task, see Walkthrough: Removing Work from a User-Interface Thread.

After you call cancel, the task group does not start future tasks. The cancel methods can be called by multiple child tasks. A canceled task causes the Concurrency::task_group::wait and Concurrency::structured_task_group::wait methods to return Concurrency::canceled.

The cancel method only affects child tasks. For example, if you cancel the task group tg1 in the illustration of the parallel work tree, all tasks in the tree (t1, t2, t3, t4, and t5) are affected. If you cancel the nested task group, tg2, only tasks t4 and t5 are affected.

When you call the cancel method, all child task groups are also canceled. However, cancellation does not affect any parents of the task group in a parallel work tree. The following examples show this by building on the parallel work tree illustration.

The first of these examples creates a work function for the task t4, which is a child of the task group tg2. The work function calls the function work in a loop. If any call to work fails, the task cancels its parent task group. This causes task group tg2 to enter the canceled state, but it does not cancel task group 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;
      }
   }         
});

This second example resembles the first one, except that the task cancels task group tg1. This affects all tasks in the tree (t1, t2, t3, t4, and 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;
      }
   }   
});

The structured_task_group class is not thread-safe. Therefore, a child task that calls a method of its parent structured_task_group object produces unspecified behavior. The exceptions to this rule are the structured_task_group::cancel and Concurrency::structured_task_group::is_canceling methods. A child task can call these methods to cancel the parent task and check for cancellation.

Using Exceptions to Cancel Parallel Work

The topic Exception Handling in the Concurrency Runtime explains how the Concurrency Runtime uses exceptions to communicate errors. However, not all exceptions indicate an error. For example, a search algorithm might cancel its associated task group when it finds the result. However, as mentioned previously, exception handling is less efficient than using the cancel method to cancel parallel work.

When you throw an exception in the body of a work function that you pass to a task group, the runtime stores that exception and marshals the exception to the context that waits for the task group to finish. As with the cancel method, the runtime discards any tasks that have not yet started, and does not accept new tasks.

This third example resembles the second one, except that task t4 throws an exception to cancel the task group tg2. This example uses a try-catch block to check for cancellation when the task group tg2 waits for its child tasks to finish. Like the first example, this causes the task group tg2 to enter the canceled state, but it does not cancel task group 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;
}

This fourth example uses exception handling to cancel the whole work tree. The example catches the exception when task group tg1 waits for its child tasks to finish instead of when task group tg2 waits for its child tasks. Like the second example, this causes both tasks groups in the tree, tg1 and tg2, to enter the canceled state.

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

Because the task_group::wait and structured_task_group::wait methods throw when a child task throws an exception, you do not receive a return value from them.

Determining When Work Is Canceled

Cancellation is cooperative. Therefore, it does not occur immediately. If a task group is canceled, calls from each child task into the runtime can trigger an interruption point, which causes the runtime to throw and catch an internal exception type to cancel active tasks. The Concurrency Runtime does not define specific interruption points; they can occur in any call to the runtime. The runtime must handle the exceptions that it throws in order to perform cancellation. Therefore, do not handle unknown exceptions in the body of a task.

If a child task performs a time-consuming operation and does not call into the runtime, it must periodically check for cancellation and exit in a timely manner. The following example shows one way to determine when work is canceled. Task t4 cancels the parent task group when it encounters an error. Task t5 occasionally calls the structured_task_group::is_canceling method to check for cancellation. If the parent task group is canceled, task t5 prints a message and exits.

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();

This example checks for cancellation on every 100th iteration of the task loop. The frequency with which you check for cancellation depends on the amount of work your task performs and how quickly you need for tasks to respond to cancellation.

If you do not have access to the parent task group object, call the Concurrency::is_current_task_group_canceling function to determine whether the parent task group is canceled.

[go to top]

Canceling Parallel Algorithms

Parallel algorithms in the PPL, for example, Concurrency::parallel_for, build on task groups. Therefore, you can use many of the same techniques to cancel a parallel algorithm.

The following examples illustrate several ways to cancel a parallel algorithm.

The following example uses the Concurrency::structured_task_group::run_and_wait method to call the parallel_for algorithm. The structured_task_group::run_and_wait method waits for the provided task to finish. The structured_task_group object enables the work function to cancel the task.

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

This example produces the following output.

The task group status is: canceled.

The following example uses exception handling to cancel a parallel_for loop. The runtime marshals the exception to the calling context.

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

This example produces the following output.

Caught 50

The following example uses a Boolean flag to coordinate cancellation in a parallel_for loop. Every task runs because this example does not use the cancel method or exception handling to cancel the overall set of tasks. Therefore, this technique can have more computational overhead than a cancelation mechanism.

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

Each cancellation method has advantages over the others. Choose the method that fits your specific needs.

[go to top]

When Not to Use Cancellation

The use of cancellation is appropriate when each member of a group of related tasks can exit in a timely manner. However, there are some scenarios where cancellation may not be appropriate for your application. For example, because task cancellation is cooperative, the overall set of tasks will not cancel if any individual task is blocked. For example, if one task has not yet started, but it unblocks another active task, it will not start if the task group is canceled. This can cause deadlock to occur in your application. A second example of where the use of cancellation may not be appropriate is when a task is canceled, but its child task performs an important operation, such as freeing a resource. Because the overall set of tasks is canceled when the parent task is canceled, that operation will not execute. For an example that illustrates this point, see the Understand how Cancellation and Exception Handling Affect Object Destruction section in the Best Practices in the Parallel Patterns Library topic.

[go to top]

Reference

task_group Class

structured_task_group Class

parallel_for Function

Change History

Date

History

Reason

March 2011

Added another case to the When Not to Use Cancellation section.

Information enhancement.