PPL 中的取消操作
本文档说明并行模式库 (PPL) 中取消操作的角色、如何取消并行工作以及如何确定取消并行工作的时间。
备注
运行时使用异常处理实现取消操作。不要在代码中捕捉或处理这些异常。此外,还建议您在任务的函数体中编写异常安全的代码。例如,可以使用“获取资源即初始化”(RAII) 模式,以确保在任务体中引发异常时正确处理资源。有关使用 RAII 模式清理可取消任务中的资源的完整示例,请参阅演练:从用户界面线程中移除工作。
关键点
取消是协作性的并且涉及在请求取消的代码和响应取消的任务之间的协作。
如有可能,使用取消标记取消工作。 concurrency::cancellation_token 类定义取消标记。
当使用取消标记,请使用 concurrency::cancellation_token_source::cancel 方法初始化取消和 concurrency::is_task_cancellation_requested concurrency::cancel_current_task 函数和响应取消。
因此,取消不会立即发生。 如果任务或任务组已取消,即使新的工作未启动,活动的工作必须检查和响应取消。
基于值的继承延续前面任务的取消标记。 基于任务的继续不会继承其前面的任务标记。
当您调用采用 cancellation_token 对象的构造函数或函数,但您希望操作不是取消时,请使用 concurrency::cancellation_token::none 方法。 此外,如果未将此取消标记传递给 concurrency::task 构造函数或concurrency::create_task函数,该任务是不可取消的。
在本文档中
并行工作树
取消并行任务
使用取消标记来取消并行工作
使用取消方法来取消并行工作
使用异常来取消并行工作
取消并行算法
何时不使用取消
并行工作树
PPL 使用任务和任务组来管理细化的任务和计算。 可以嵌套任务组,以形成并行工作树。 下图演示了并行工作树。 在该图中,tg1 和 tg2 表示任务组;t1、t2、t3、t4和 t5 表示任务组执行的工作。
以下示例演示了创建该图中的树所需的代码。 在此示例中,tg1 和 tg2 是 concurrency::structured_task_group 对象;t1、t2、t3、t4 和 t5 是 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();
}
还可以使用 concurrency::task_group 类创建的类似工作树。 concurrency::task 类还支持工作树的概念。 但是,task 树是依赖树。 在 task 树,将来工作在当前工作后完成。 在任务组树中,内部工作在外部工作之前完成。 有关任务和任务组之间差异的更多信息,请参见任务并行(并发运行时)。
[Top]
取消并行任务
可以通过多种方式取消并行工作。 首选方法是使用取消标记。 任务组同时支持 concurrency::task_group::cancel 方法和 才、concurrency::structured_task_group::cancel 方法。 最后一种方式是在任务工作函数体中引发异常。 无论选择哪个方法,必须了解了取消不会立即发生。 如果任务或任务组已取消,即使新的工作未启动,活动的工作必须检查和响应取消。
有关取消并行任务的更多示例,请参见演练:使用任务和 XML HTTP 请求进行连接, 如何:使用取消中断 Parallel 循环,和如何:使用异常处理中断 Parallel 循环.。
使用取消标记来取消并行工作
task, task_group, 和 structured_task_group 类支持通过使用取消标记进行取消。 PPL 为该目标定义和 concurrency::cancellation_token_source concurrency::cancellation_token 类。 当使用一个取消标记来取消工作时,运行时不会开始订阅该标记的新工作。 已经激活的工作可监视其取消标记以及当它可以的时候停止。
若要初始化取消,请调用concurrency::cancellation_token_source::cancel方法。 响应取消可以有以下方法:
对于 task 对象,请使用 concurrency::is_task_cancellation_requested 和concurrency::cancel_current_task 函数。 cancel_current_task 取消它当前任务和任何一个基于值的延续。(它不取消与任务或与它的延续的相关的取消 标记。)
对于任务组和并行算法,那么,当该函数返回 true时,请使用 concurrency::is_current_task_group_canceling 函数检测"取消"并从任务正文尽快返回。(请勿从任务组调用cancel_current_task。)
下面的示例演示取消任务的第一基本模式。 任务正文偶尔检查在循环中的取消。
// 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_requested 和 cancel_current_task。
当响应取消时,因为任务状态转换为 Canceled 状态,调用cancel_current_task 非常重要。 如果提前返回而不调用 cancel_current_task,操作转换为已完成状态并且所有基于值的延续运行。
警告
请勿从代码抛出 task_canceled。调用 cancel_current_task 替代。
当任务以取消状态结束, concurrency::task_canceled方法抛出concurrency::task::get 。(反之,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 对象,进行以下的方法继承前面的任务取消标记:
基于值的延续始终继承前面任务的取消标记。
基于任务的继续不会继承其前面的取消标记。 使基于任务的延续可取消的唯一方法是显式传递一个取消标记。
这些行为不会受出错任务(即引发异常) 影响。 在这种情况下,基于值延续被取消;基于任务的延续任务不会被取消。
警告
在另一个任务中创建的任务 (即,嵌套任务) 不会继承父任务的取消标记。只有基于值的继承延续前面任务的取消标记。
提示
当您调用采用 cancellation_token 对象的构造函数或函数,并且不希望操作是取消时,请使用 concurrency::cancellation_token::none 方法。
还可以提供取消标记到 task_group 或 structured_task_group 对象的构造函数。 一个重要方面是子任务组继承此取消标记。 有关演示此概念可以使用 concurrency::run_with_cancellation_token 函数运行调用 parallel_for的示例,请稍后参见 取消并行算法 文档。
[Top]
取消标记和任务复合
concurrency::when_all 和 concurrency::when_any 函数,这些函数有助于您撰写多个任务以实现常见模式。 本节描述这些函数如何与取消标记一起使用。
当提供一个取消标记传递到任意 when_all 和 when_any 函数,只有当该取消标记为已取消,或者某个参与任务已取消或引发异常的情况该函数删除,。
在不提供一个取消标记给它时,when_all 函数从复合整体操作的每项任务继承取消标记。 当所有取消标记为已取消并且一个参与者任务都尚未启动或运行,when_all 返回的任务取消。 当任意一个任务抛出异常时,类似的行为发生—— when_all 返回的任务立即因该异常取消。
当任务完成时,运行时会为when_any 函数返回的任务选择取消标记。 如果参与者任务都不在完成状态完成,并且一个或多个任务引发异常时,引发的任务之一被选择完成 when_any 同时其标记作为最终任务的标记。 如果多个任务在完成状态完成,从 when_any 返回的任务将在完成状态下结束。 运行时在完成时尝试选取标记不是已取消的任务,以便从 when_any 返回的任务不会立即取消,即使其他执行的任务可能晚一点完成。
[Top]
使用取消方法来取消并行工作
concurrency::task_group::cancel 和 concurrency::structured_task_group::cancel 方法可将任务组设置为已取消状态。 在您调用 cancel 后,任务组不会启动将来的任务。 cancel 方法可以由多个子任务调用。 已取消状态会导致 concurrency::task_group::wait 和 concurrency::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,则该树中的所有任务(t1、t2、t3、t4 和 t5)都将受到影响。 如果取消嵌套的任务组 tg2,则只有任务 t4 和 t5 会受到影响。
当您调用 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。 这会影响树中的所有任务(t1、t2、t3、t4 和 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;
}
}
});
structured_task_group 类不是线程安全的。 因此,调用其父 structured_task_group 对象方法的子任务会发生未指定的行为。 此规则的例外是 structured_task_group::cancel 和 concurrency::structured_task_group::is_canceling 方法。 子任务可以调用这些方法来取消父任务组和检查取消。
警告
尽管可以使用取消标记来取消由任务组运行作为 task 对象的子工作,但不能使用 task_group::cancel 或 structured_task_group::cancel 方法移除该任务组中运行的 task 对象。
[Top]
使用异常来取消并行工作
在取消并行工作树时,使用取消标记与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 等待其子任务完成时)捕获异常。 与第二个示例类似,这会导致树中的两个任务组 tg1 和 tg2 都进入已取消状态。
// 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::wait 和 structured_task_group::wait 方法在子任务引发异常时引发,所以您没有从它们收到返回值。
[Top]
取消并行算法
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;
}
该示例产生下面的输出。
下面的示例使用异常处理来取消 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;
}
该示例产生下面的输出。
下面的示例使用一个布尔型标志来协调 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.
}
});
每个取消方法都有其他方法所没有的优点。 请选择适合您的特定需求的方法。
[Top]
何时不使用取消
当一组相关任务中的每个成员可以及时退出时,使用取消是恰当的。 但是,在某些情况下取消可能不适合您的应用程序。 例如,由于任务取消是协作性的,如果任何单个任务被阻止,则无法取消整个任务集。 例如,如果一个任务尚未开始,但它取消阻止另一个活动任务,则如果任务组已取消,它将不能启动。 这会导致应用程序中发生死锁。 可能不适合使用取消机制的另一个示例是在取消任务时,但其子任务会执行重要操作(如释放资源)。 因为在取消父任务时整个任务集也会被取消,所以将无法执行此操作。 有关说明这个观点的示例,请参阅并行模式库主题中的最佳实践中的了解取消和异常处理如何影响对象销毁部分。
[Top]
相关主题
标题 |
说明 |
---|---|
演示如何使用取消来实现并行搜索算法。 |
|
演示如何使用 task_group 类编写基本树结构的搜索算法 |
|
描述运行时如何处理任务组、轻量级任务和异步代理引发的异常,以及如何在应用程序中响应异常。 |
|
介绍任务与任务组之间的关系,以及如何在应用程序中使用非结构化和结构化的任务。 |
|
描述同时对多个数据集合执行操作的并行算法 |
|
提供对并行模式库的概述。 |