Dela via


Cancellation in the PPL

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

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.

Key Points

  • Cancellation is cooperative and involves coordination between the code that requests cancellation and the task that responds to cancellation.

  • When possible, use cancellation tokens to cancel work. The concurrency::cancellation_token class defines a cancellation token.

  • When you use cancellation tokens, use the concurrency::cancellation_token_source::cancel method to initiate cancellation and the concurrency::is_task_cancellation_requested and concurrency::cancel_current_task functions to respond to cancellation.

  • Cancellation does not occur immediately. Although new work is not started if a task or task group is cancelled, active work must check for and respond to cancellation.

  • A value-based continuation inherits the cancellation token of its antecedent task. A task-based continuation never inherits the token of its antecedent task.

  • Use the concurrency::cancellation_token::none method when you call a constructor or function that takes a cancellation_token object but you do not want the operation to be cancellable. Also, if you do not pass a cancellation token to the concurrency::task constructor or the concurrency::create_task function, that task is not cancellable.

In this Document

  • Parallel Work Trees

  • Canceling Parallel Tasks

    • Using a Cancellation Token to Cancel Parallel Work

    • Using the cancel Method to Cancel Parallel Work

    • Using Exceptions to Cancel Parallel Work

  • Canceling Parallel Algorithms

  • When Not to Use Cancellation

Parallel Work Trees

The PPL uses tasks and 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 the work that the task groups perform.

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

You can also use the concurrency::task_group class to create a similar work tree. The concurrency::task class also supports the notion of a tree of work. However, a task tree is a dependency tree. In a task tree, future works completes after current work. In a task group tree, internal work completes before outer work. For more information about the differences between tasks and task groups, see Task Parallelism (Concurrency Runtime).

[Top]

Canceling Parallel Tasks

There are multiple ways to cancel parallel work. The preferred way is to use a cancellation token. Task groups also support the concurrency::task_group::cancel method and the concurrency::structured_task_group::cancel method. The final way is to throw an exception in the body of a task work function. No matter which method you choose, understand that cancellation does not occur immediately. Although new work is not started if a task or task group is cancelled, active work must check for and respond to cancellation.

For more examples that cancel parallel tasks, see Walkthrough: Connecting Using Tasks and XML HTTP Requests, How to: Use Cancellation to Break from a Parallel Loop, and How to: Use Exception Handling to Break from a Parallel Loop.

Using a Cancellation Token to Cancel Parallel Work

The task, task_group, and structured_task_group classes support cancellation through the use of cancellation tokens. The PPL defines the concurrency::cancellation_token_source and concurrency::cancellation_token classes for this purpose. When you use a cancellation token to cancel work, the runtime does not start new work that subscribes to that token. Work that is already active can monitor its cancellation token and stop when it can.

To initiate cancellation, call the concurrency::cancellation_token_source::cancel method. You respond to cancellation in these ways:

The following example shows the first basic pattern for task cancellation. The task body occasionally checks for cancellation inside a loop.

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

using namespace concurrency;
using namespace std;

bool do_work()
{
    // Simulate work.
    wcout << L"Performing work..." << endl;
    wait(250);
    return true;
}

int wmain()
{
    cancellation_token_source cts;
    auto token = cts.get_token();

    wcout << L"Creating task..." << endl;

    // Create a task that performs work until it is canceled.
    auto t = create_task([]
    {
        bool moreToDo = true;
        while (moreToDo)
        {
            // Check for cancellation. 
            if (is_task_cancellation_requested())
            {
                // TODO: Perform any necessary cleanup here... 

                // Cancel the current task.
                cancel_current_task();
            }
            else 
            {
                // Perform work.
                moreToDo = do_work();
            }
        }
    }, token);

    // Wait for one second and then cancel the task.
    wait(1000);

    wcout << L"Canceling task..." << endl;
    cts.cancel();

    // Wait for the task to cancel.
    wcout << L"Waiting for task to complete..." << endl;
    t.wait();

    wcout << L"Done." << endl;
}

/* Sample output:
    Creating task...
    Performing work...
    Performing work...
    Performing work...
    Performing work...
    Canceling task...
    Waiting for task to complete...
    Done.
*/

The cancel_current_task function throws; therefore, you do not need to explicitly return from the current loop or function.

Tip

Alternatively, you can call the concurrency::interruption_point function instead of is_task_cancellation_requested and cancel_current_task.

It is important to call cancel_current_task when you respond to cancellation because it transitions the task to the canceled state. If you return early instead of calling cancel_current_task, the operation transitions to the completed state and any value-based continuations are run.

Warning

Never throw task_canceled from your code. Call cancel_current_task instead.

When a task ends in the canceled state, the concurrency::task::get method throws concurrency::task_canceled. (Conversely, concurrency::task::wait returns task_status::canceled and does not throw.) The following example illustrates this behavior for a task-based continuation. A task-based continuation is always called, even when the antecedent task is canceled.

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

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t1 = create_task([]() -> int
    {
        // Cancel the task.
        cancel_current_task();
    });

    // Create a continuation that retrieves the value from the previous.
    auto t2 = t1.then([](task<int> t)
    {
        try
        {
            int n = t.get();
            wcout << L"The previous task returned " << n << L'.' << endl;
        }
        catch (const task_canceled& e)
        {
            wcout << L"The previous task was canceled." << endl;
        }
    });

    // Wait for all tasks to complete.
    t2.wait();
}
/* Output:
    The previous task was canceled.
*/

Because value-based continuations inherit the token of their antecedent task unless they were created with an explicit token, the continuations immediately enter the canceled state even when the antecedent task is still executing. Therefore, any exception that is thrown by the antecedent task after cancellation is not propagated to the continuation tasks. Cancellation always overrides the state of the antecedent task. The following example resembles the previous, but illustrates the behavior for a value-based continuation.

auto t1 = create_task([]() -> int
{
    // Cancel the task.
    cancel_current_task();
});

// Create a continuation that retrieves the value from the previous.
auto t2 = t1.then([](int n)
{
    wcout << L"The previous task returned " << n << L'.' << endl;
});

try
{
    // Wait for all tasks to complete.
    t2.get();
}
catch (const task_canceled& e)
{
    wcout << L"The task was canceled." << endl;
}
/* Output:
    The task was canceled.
*/

Warning

If you do not pass a cancellation token to the task constructor or the concurrency::create_task function, that task is not cancellable. In addition, you must pass the same cancellation token to the constructor of any nested tasks (that is, tasks that are created in the body of another task) to cancel all tasks simultaneously.

You might want to run arbitrary code when a cancellation token is canceled. For example, if your user chooses a Cancel button on the user interface to cancel the operation, you could disable that button until the user starts another operation. The following example shows how to use the concurrency::cancellation_token::register_callback method to register a callback function that runs when a cancellation token is canceled.

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

using namespace concurrency;
using namespace std;

int wmain()
{
    cancellation_token_source cts;
    auto token = cts.get_token();

    // An event that is set in the cancellation callback. 
    event e;

    cancellation_token_registration cookie;
    cookie = token.register_callback([&e, token, &cookie]()
    {
        wcout << L"In cancellation callback..." << endl;
        e.set();

        // Although not required, demonstrate how to unregister  
        // the callback.
        token.deregister_callback(cookie);
    });

    wcout << L"Creating task..." << endl;

    // Create a task that waits to be canceled.
    auto t = create_task([&e]
    {
        e.wait();
    }, token);

    // Cancel the task.
    wcout << L"Canceling task..." << endl;
    cts.cancel();

    // Wait for the task to cancel.
    t.wait();

    wcout << L"Done." << endl;
}
/* Sample output:
    Creating task...
    Canceling task...
    In cancellation callback...
    Done.
*/

The document Task Parallelism (Concurrency Runtime) explains the difference between value-based and task-based continuations. If you do not provide a cancellation_token object to a continuation task, the continuation inherits the cancellation token from the antecedent task in the following ways:

  • A value-based continuation always inherits the cancellation token of the antecedent task.

  • A task-based continuation never inherits the cancellation token of the antecedent task. The only way to make a task-based continuation cancelable is to explicitly pass a cancellation token.

These behaviors are not affected by a faulted task (that is, one that throws an exception). In this case, a value-based continuation is cancelled; a task-based continuation is not cancelled.

Warning

A task that is created in another task (in other words, a nested task) does not inherit the cancellation token of the parent task. Only a value-based continuation inherits the cancellation token of its antecedent task.

Tip

Use the concurrency::cancellation_token::none method when you call a constructor or function that takes a cancellation_token object and you do not want the operation to be cancellable.

You can also provide a cancellation token to the constructor of a task_group or structured_task_group object. An important aspect of this is that child task groups inherit this cancellation token. For an example that demonstrates this concept by using the concurrency::run_with_cancellation_token function to run to call parallel_for, see Canceling Parallel Algorithms later in this document.

[Top]

Cancellation Tokens and Task Composition

The concurrency::when_all and concurrency::when_any functions can help you compose multiple tasks to implement common patterns. This section describes how these functions work with cancellation tokens.

When you provide a cancellation token to either the when_all and when_any function, that function cancels only when that cancellation token is cancelled or when one of the participant tasks ends in a canceled state or throws an exception.

The when_all function inherits the cancellation token from each task that composes the overall operation when you do not provide a cancellation token to it. The task that is returned from when_all is canceled when any of these tokens are cancelled and at least one of the participant tasks has not yet started or is running. A similar behavior occurs when one of the tasks throws an exception – the task that is returned from when_all is immediately canceled with that exception.

The runtime chooses the cancellation token for the task that is returned from when_any function when that task completes. If none of the participant tasks finish in a completed state and one or more of the tasks throws an exception, one of the tasks that threw is chosen to complete the when_any and its token is chosen as the token for the final task. If more than one task finishes in the completed state, the task that is returned from when_any task ends in a completed state. The runtime tries to pick a completed task whose token is not canceled at the time of completion so that the task that is returned from when_any is not immediately canceled even though other executing tasks might complete at a later point.

[Top]

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

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.

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 group and check for cancellation.

Warning

Although you can use a cancellation token to cancel work that is performed by a task group that runs as a child of a task object, you cannot use the task_group::cancel or structured_task_group::cancel methods to cancel task objects that run in a task group.

[Top]

Using Exceptions to Cancel Parallel Work

The use of cancellation tokens and the cancel method are more efficient than exception handling at canceling a parallel work tree. Cancellation tokens and the cancel method cancel a task and any child tasks 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 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 when it finds the result. However, as mentioned previously, exception handling is less efficient than using the cancel method to cancel parallel work.

Warning

We recommend that you use exceptions to cancel parallel work only when necessary. Cancellation tokens and the task group cancel methods are more efficient and less prone to error.

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.

[Top]

Canceling Parallel Algorithms

Parallel algorithms in the PPL, for example, 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 run_with_cancellation_token function to call the parallel_for algorithm. The run_with_cancellation_token function takes a cancellation token as an argument and calls the provided work function synchronously. Because parallel algorithms are built upon tasks, they inherit the cancellation token of the parent task. Therefore, parallel_for can respond to cancellation.

// cancel-parallel-for.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Call parallel_for in the context of a cancellation token.
    cancellation_token_source cts;
    run_with_cancellation_token([&cts]() 
    {
        // Print values to the console in parallel.
        parallel_for(0, 20, [&cts](int n)
        {
            // For demonstration, cancel the overall operation  
            // when n equals 11. 
            if (n == 11)
            {
                cts.cancel();
            }
            // Otherwise, print the value. 
            else
            {
                wstringstream ss;
                ss << n << endl;
                wcout << ss.str();
            }
        });
    }, cts.get_token());
}
/* Sample output:
    15
    16
    17
    10
    0
    18
    5
*/

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.

[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.

[Top]

Title

Description

How to: Use Cancellation to Break from a Parallel Loop

Shows how to use cancellation to implement a parallel search algorithm.

How to: Use Exception Handling to Break from a Parallel Loop

Shows how to use the task_group class to write a search algorithm for a basic tree structure.

Exception Handling in the Concurrency Runtime

Describes how the runtime handles exceptions that are thrown by task groups, lightweight tasks, and asynchronous agents, and how to respond to exceptions in your applications.

Task Parallelism (Concurrency Runtime)

Describes how tasks relate to task groups and how you can use unstructured and structured tasks in your applications.

Parallel Algorithms

Describes the parallel algorithms, which concurrently perform work on collections of data

Parallel Patterns Library (PPL)

Provides an overview of the Parallel Patterns Library.

Reference

task Class (Concurrency Runtime)

cancellation_token_source Class

cancellation_token Class

task_group Class

structured_task_group Class

parallel_for Function