PPL 中的取消操作

在并行模式库(PPL)中文档解释取消操作的效果,如何取消并行工作以及如何确定并行取消工作的时间。

备注

运行时使用异常处理实现取消操作。不要在代码中捕捉或处理这些异常。此外,还建议您在任务的函数体中编写异常安全的代码。例如,可以使用“获取资源即初始化”(RAII) 模式,以确保在任务体中引发异常时正确处理资源。有关使用 RAII 模式清理可取消任务中的资源的完整示例,请参阅演练:从用户界面线程中移除工作

关键点

在本文档

  • 并行工作树

  • 取消并行任务

    • 使用取消标记来取消并行工作

    • 使用取消方法来取消并行工作

    • 使用异常来取消并行工作

  • 取消并行算法

  • 何时不使用取消

并行工作树

PPL使用任务和任务组来管理细化的任务和计算。 可以嵌套任务组,以形成并行工作树。 下图演示了并行工作树。 在此图中,tg1tg2 表示任务组; t1t2t3t4t5 表示任务组执行的工作。

并行工作树

以下示例演示了创建该图中的树所需的代码。 在此示例中,tg1tg2concurrency::structured_task_group 对象; t1t2t3t4t5concurrency::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();   
}

还可以使用 concurrency::task_group 选件类创建一个类似的工作树。 concurrency::task 选件类还支持工作树的概念。 但是,task 树是依赖项树。 在 task 树,后面的工作在当前工作完成之后。 在任务组树,内部工作原理在外部工作之前完成。 有关任务和任务组之间的差异的更多信息,请参见 任务并行(并发运行时)

[顶级]

取消并行任务

有多种方式取消并行工作。 首选方法是使用取消标记。 任务组还支持 concurrency::task_group::cancel 方法和 concurrency::structured_task_group::cancel 方法。 最终方法都将在任务工作函数体中引发异常。 所选方法,了解哪取消不会立即发生。 虽然新的工作不启动,如果任务或任务组已取消,活动工作必须检查和响应取消。

有关取消并行任务的更多示例,请参见 演练:使用任务和 XML HTTP 请求 (IXHR2) 进行连接如何:使用取消中断 Parallel 循环如何:使用异常处理中断 Parallel 循环

Dd984117.collapse_all(zh-cn,VS.110).gif使用取消标记来取消并行工作

tasktask_groupstructured_task_group 选件类通过使用取消标记支持取消。 PPL为此定义 concurrency::cancellation_token_sourceconcurrency::cancellation_token 选件类。 当使用一个取消标记来取消工作时,运行时不会开始订阅该标记的新工作。 工作已经有效可监视其取消标记和停止,则可以。

若要启动取消,请调用 concurrency::cancellation_token_source::cancel 方法。 您可以响应取消的方法如下:

下面的示例演示任务取消的第一个基本模式。 任务体偶尔检查在循环内的取消。

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

cancel_current_task 函数引发;因此,不必从该当前线圈或函数显式返回。

提示

或者,可以调用 concurrency::interruption_point 功能而不是 is_task_cancellation_requestedcancel_current_task

在响应取消,因为它转换任务与为已取消状态时,调用 cancel_current_task 非常重要。 如果早已返回而不是调用 cancel_current_task,与已完成状态的操作转换和所有基于的值继续运行。

警告

不要引发从代码中 task_canceled。调用 cancel_current_task

在为已取消状态的任务结束,concurrency::task::get 方法引发 concurrency::task_canceled。 (反之,concurrency::task::wait 返回 task_status::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.
*/

由于基于值的延续继承其前面的任务标记,除非他们将显式标记创建的,继续立即进入已取消状态,即使在前面的任务仍将执行。 因此,由前面的任务引发的任何异常,取消不会传播到延续任务后。 取消始终重写前面的任务的状态。 下面的示例与前面的示例类似,但是,声明基于值的延续的行为。

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

警告

如果没有通过取消标记传递给 task 构造函数或 concurrency::create_task 功能,该任务不是取消。此外,还必须将相同的取消标记传递到在另一个任务体中创建)所有嵌套任务(即任务的构造函数同时移除所有任务。

当取消标记中移除时,您可能希望运行任意代码。 例如,在中,如果用户选择在用户界面中一个 *** 取消 *** 按钮取消该操作,则可以禁用该按钮,直到用户启动另一个操作。 下面的示例演示如何使用 concurrency::cancellation_token::register_callback 方法注册运行的回调函数,当取消标记取消时。

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

文档 任务并行(并发运行时) 解释基于值的和基于任务的延续之间的差异。 如果未提供到延续任务的一 cancellation_token 对象,继续下面的方法继承从前面的任务取消标记:

  • 基于值的延续始终继承前面的任务取消标记。

  • 基于任务的延续不继承前面的任务取消标记。 唯一的方式使基于任务的延续取消将显式将取消标记。

这些行为不影响的受已出错的任务(即引发异常)中的。 在这种情况下,基于值的延续取消;基于任务的延续不会被取消。

警告

在另一个任务换言之,的任务(嵌套任务)创建不继承父任务的取消标记。仅基于值的延续继承其前面的任务取消标记。

提示

请使用 concurrency::cancellation_token::none 方法,请在调用构造函数时或者将 cancellation_token 对象的功能和不希望操作将取消。

您还可以提供取消标记传递到 task_groupstructured_task_group 对象的构造函数。 这种情况的一个重要方面是子任务组继承此取消标记。 有关演示此概念使用 concurrency::run_with_cancellation_token 功能运行调用 parallel_for的示例,请参见 取消并行算法 后文档中。

[顶级]

Dd984117.collapse_all(zh-cn,VS.110).gif取消标记和任务组合

concurrency::when_allconcurrency::when_any 功能有助于组合多个任务实现常见模式。 本节描述这些功能如何使用取消标记一起使用。

当提供一个取消标记对任何 when_allwhen_any 功能,该功能来撤消中,只有在取消标记被取消或,当某个已取消状态的参与者任务关闭或引发异常。

when_all 功能继承组合整体操作的每个任务的取消标记,不提供一个取消标记传递到它。 从 when_all 返回的任务已取消,当这些标记中的任何一个取消时和任务尚未开始或运行的至少一个参与者。 一个类似的行为发生,当某个任务引发异常–从 when_all 返回的任务立即取消对该异常。

运行时选择从 when_any 函数返回的任务的取消标记,当该任务完成。 如果参与者任务都在一个已完成状态未完成,以及一个或多个任务引发异常,引发的任务之一中选择完成 when_any 及其标记中选择作为最后任务的标记。 如果多个任务在已完成状态完成,从 when_any 在完整状态的任务结束返回的任务。 运行时会选择标记不在完成时取消的已完成的任务,以便从 when_any 返回的任务不会立即被取消,即使执行其他任务可能完成在以后。

[顶级]

Dd984117.collapse_all(zh-cn,VS.110).gif使用取消方法来取消并行工作

concurrency::task_group::cancelconcurrency::structured_task_group::cancel 方法将任务组设置为已取消状态。 在您调用 cancel 后,任务组不会启动将来的任务。 cancel 方法可以由多个子任务调用。 已取消的任务会导致 concurrency::task_group::waitconcurrency::structured_task_group::wait 方法返回 concurrency::canceled

如果任务组已取消,从每个子任务到运行时的调用可以触发一个中断点,这将导致运行时引发和捕获内部异常类型以取消活动任务。 并发运行时不定义具体中断点;它们可以在对运行时的任何调用中出现。 运行时必须处理它引发的异常,才能执行取消。 因此,请不要处理任务体中的未知异常。

如果子任务执行耗时的操作,并且不调用运行时,它必须定期检查取消并及时退出。 下面的示例演示一种确定取消工作的时间的方法。 任务 t4 在遇到错误时取消父任务组。 任务 t5 偶尔调用 structured_task_group::is_canceling 方法来检查取消。 如果父任务组已取消,任务 t5 打印一条消息并退出。

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

此示例在任务循环每迭代 100 次时检查取消。 检查取消的频率取决于任务执行的工作量和您需要任务响应取消的速度。

如果您无权访问父任务组对象,请调用 concurrency::is_current_task_group_canceling 函数确定是否已取消父任务组。

cancel 方法只影响子任务。 例如,如果取消并行工作树插图中的任务组 tg1,则该树中的所有任务(t1t2t3t4t5)都将受到影响。 如果取消嵌套的任务组 tg2,则只有任务 t4t5 会受到影响。

当您调用 cancel 方法时,将同时取消所有子任务组。 但是,取消操作并不影响并行工作树中任务组的任何父级。 下面的示例通过在并行工作树插图中生成来演示这一点。

这些示例中的第一个示例为任务 t4(该任务是任务组 tg2 的子级)创建一个工作函数。 该工作函数在循环中调用函数 work。 如果对 work 的任何调用失败,则该任务取消其父任务组。 这将导致任务组 tg2 进入已取消状态,但不会取消任务组 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;
      }
   }         
});

第二个示例与第一个示例类似,只不过该任务将取消任务组 tg1。 这会影响树中的所有任务(t1t2t3t4t5)。

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

structured_task_group 类不是线程安全的。 因此,调用其父 structured_task_group 对象方法的子任务会发生未指定的行为。 此规则的例外情况是 structured_task_group::cancelconcurrency::structured_task_group::is_canceling 方法。 子任务可以调用这些方法来取消父任务组和检查取消。

警告

尽管可以使用取消标记来取消由任务组执行身份运行 task 对象的子级的工作,不能使用 task_group::cancelstructured_task_group::cancel 方法取消该任务组中运行的 task 对象。

[顶级]

Dd984117.collapse_all(zh-cn,VS.110).gif使用异常来取消并行工作

使用取消标记和 cancel 方法比异常处理有效在取消并行工作树。 取消标记和 cancel 方法取消任务和子任务以自上而下的方式。 相反,异常处理以自下而上的方式工作,并且必须在异常向上传播时单独取消每个子任务组。 并发运行时中的异常处理主题解释并发运行时如何使用异常来传递错误。 但是,并非所有异常都表示错误。 例如,在中,在找到结果时,搜索算法可能取消其关联的任务。 但是,如上所述,在取消并行工作时,异常处理的效率比使用 cancel 方法低。

警告

建议您使用异常来取消并行工作,仅在必要时。取消标记和任务组 cancel 方法更加高效且不易出错。

当您在传递给任务组的工作函数体中引发异常时,运行时存储该异常,并将该异常封送到等待任务组完成的上下文。 像 cancel 方法一样,运行时将放弃任何尚未启动的任务,并且不接受新任务。

第三个示例与第二个示例类似,只不过任务 t4 引发异常来取消任务组 tg2。 此示例使用 try-catch 块在任务组 tg2 等待其子任务完成时检查取消操作。 与第一个示例类似,这会导致任务组 tg2 进入已取消状态,但是它不取消任务组 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;
}

第四个示例使用异常处理来取消整个工作树。 此示例在任务组 tg1 等待其子任务完成时(而不是任务组 tg2 等待其子任务完成时)捕获异常。 与第二个示例类似,这会导致树中的两个任务组 tg1tg2 都进入已取消状态。

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

因为 task_group::waitstructured_task_group::wait 方法在子任务引发异常时引发,所以您没有从它们收到返回值。

[顶级]

取消并行算法

PPL的并行算法,例如,parallel_for,在任务组的生成。 因此,您可以使用许多相同的技术来取消并行算法。

下面的示例说明了几种取消并行算法的方法。

下面的示例使用 run_with_cancellation_token 函数调用 parallel_for 算法。 run_with_cancellation_token 函数采用取消标记作为参数以及同步调用提供的工作函数。 因为并行算法生成在任务,它们继承父任务的取消标记。 因此,parallel_for 可以响应取消。

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

下面的示例使用 concurrency::structured_task_group::run_and_wait 方法调用 parallel_for 算法。 structured_task_group::run_and_wait 方法等待提供的任务完成。 通过 structured_task_group 对象,工作函数可以取消该任务。

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

该示例产生下面的输出。

The task group status is: canceled.

下面的示例使用异常处理来取消 parallel_for 循环。 运行时将异常封送到调用上下文。

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

该示例产生下面的输出。

Caught 50

下面的示例使用一个布尔型标志来协调 parallel_for 循环中的取消。 每个任务都运行,因为此示例不使用 cancel 方法或异常处理来取消整个任务集。 因此,这种技术的计算开销可能比取消机制大。

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

每个取消方法都有其他方法所没有的优点。 请选择适合您的特定需求的方法。

[顶级]

何时不使用取消

当一组相关任务中的每个成员可以及时退出时,使用取消是恰当的。 但是,在某些情况下取消可能不适合您的应用程序。 例如,由于任务取消是协作性的,如果任何单个任务被阻止,则无法取消整个任务集。 例如,如果一个任务尚未开始,但它取消阻止另一个活动任务,则如果任务组已取消,它将不能启动。 这会导致应用程序中发生死锁。 可能不适合使用取消机制的另一个示例是在取消任务时,但其子任务会执行重要操作(如释放资源)。 因为在取消父任务时整个任务集也会被取消,所以将无法执行此操作。 有关说明这一点的示例,请参见“并行模式库中的最佳实践”主题中的Understand how Cancellation and Exception Handling Affect Object Destruction一节。

[顶级]

相关主题

标题

说明

如何:使用取消中断 Parallel 循环

演示如何使用取消来实现并行搜索算法。

如何:使用异常处理中断 Parallel 循环

演示如何使用 task_group 选件类编写基本树结构的搜索算法。

并发运行时中的异常处理

描述运行时如何处理任务组、轻量级任务和异步代理引发的异常,以及如何在应用程序中响应异常。

任务并行(并发运行时)

介绍任务与任务组之间的关系,以及如何在应用程序中使用非结构化和结构化的任务。

并行算法

描述同时对多个数据集合执行操作的并行算法

并行模式库 (PPL)

提供对并行模式库的概述。

引用

task 类(并发运行时)

cancellation_token_source 类

cancellation_token 类

task_group 类

structured_task_group 类

parallel_for 函数