并发运行时使用 C++ 异常处理来传达多种错误。 这些错误包括:无效使用运行时、无法获取资源等运行时错误,以及你提供给任务和任务组的工作函数中发生的错误。 当任务或任务组引发异常时,运行时会保存该异常并将其编组到等待任务或任务组完成的上下文。 对于轻量级任务和代理等组件,运行时不会为你管理异常。 在这些情况下,你必须实现自己的异常处理机制。 本系列中描述运行时如何处理任务、任务组、轻量级任务和异步代理引发的异常,以及如何在应用程序中响应异常,其中的要点如下:
- 当任务或任务组引发异常时,运行时会保存该异常并将其编组到等待任务或任务组完成的上下文;
- 如果可能,请在对 concurrency::task::get 和 concurrency::task::wait 的每个调用中加上一个 try/catch 块,以处理可以从中恢复的错误。 如果任务发生异常并且该异常未被任务、其延续任务之一或主要应用捕获,则运行时将终止应用;
- 基于任务的延续始终会运行;无论前面的任务是否成功完成、引发异常还是被取消。 如果前面的任务引发异常或取消,则基于值的延续不会运行;
- 由于基于任务的延续始终运行,因此请考虑是否在延续链的末尾添加基于任务的延续。 这有助于保证你的代码遵守所有异常;
- 调用 concurrency::task::get 并且该任务被取消时,运行时会引发 concurrency::task_canceled;
- 运行时不管理轻量级任务和代理的异常;
任务和延续
本节介绍运行时如何处理由 concurrency::task 对象及其延续引发的异常。 有关任务和延续模型的更多信息,请参阅任务并行。
当你在传递给 task 对象的工作函数的主体中引发异常时,运行时会存储该异常并将其编组到调用 concurrency::task::get 或 concurrency::task::wait 的上下文中。 任务并行化描述了基于任务与基于值的延续,但总而言之,基于值的延续采用 T 类型的参数,而基于任务的延续采用 task<T> 类型的参数。 如果引发的任务具有一个或多个基于值的延续,则不会安排这些延续运行。 下面的示例阐释了这种行为:
// eh-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
wcout << L"Running a task..." << endl;
// Create a task that throws.
auto t = create_task([]
{
throw exception();
});
// Create a continuation that prints its input value.
auto continuation = t.then([]
{
// We do not expect this task to run because
// the antecedent task threw.
wcout << L"In continuation task..." << endl;
});
// Wait for the continuation to finish and handle any
// error that occurs.
try
{
wcout << L"Waiting for tasks to finish..." << endl;
continuation.wait();
// Alternatively, call get() to produce the same result.
//continuation.get();
}
catch (const exception& e)
{
wcout << L"Caught exception." << endl;
}
}
/* Output:
Running a task...
Waiting for tasks to finish...
Caught exception.
*/
基于任务的延续使你能够处理由先前任务引发的任何异常。 基于任务的延续始终会运行;无论任务是否成功完成、引发异常还是被取消。 当任务引发异常时,其基于任务的延续将计划运行。 下面的示例显示了一个始终引发的任务。 该任务有两个延续;一个任务是基于值的,另一个任务是基于任务的。 基于任务的异常始终会运行,因此可以捕获先前任务引发的异常。 当示例等待两个延续完成时,再次引发异常,因为在调用 task::get 或 task::wait 时始终会引发任务异常。
// eh-continuations.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
wcout << L"Running a task..." << endl;
// Create a task that throws.
auto t = create_task([]() -> int
{
throw exception();
return 42;
});
//
// Attach two continuations to the task. The first continuation is
// value-based; the second is task-based.
// Value-based continuation.
auto c1 = t.then([](int n)
{
// We don't expect to get here because the antecedent
// task always throws.
wcout << L"Received " << n << L'.' << endl;
});
// Task-based continuation.
auto c2 = t.then([](task<int> previousTask)
{
// We do expect to get here because task-based continuations
// are scheduled even when the antecedent task throws.
try
{
wcout << L"Received " << previousTask.get() << L'.' << endl;
}
catch (const exception& e)
{
wcout << L"Caught exception from previous task." << endl;
}
});
// Wait for the continuations to finish.
try
{
wcout << L"Waiting for tasks to finish..." << endl;
(c1 && c2).wait();
}
catch (const exception& e)
{
wcout << L"Caught exception while waiting for all tasks to finish." << endl;
}
}
/* Output:
Running a task...
Waiting for tasks to finish...
Caught exception from previous task.
Caught exception while waiting for all tasks to finish.
*/
我们建议你使用基于任务的延续来捕获你能够处理的异常。 由于基于任务的延续始终运行,因此请考虑是否在延续链的末尾添加基于任务的延续。 这有助于保证你的代码遵守所有异常。 以下示例显示了一个基本的基于值的延续链。 链中的第三个任务引发异常,因此它后面的任何基于值的延续都不会运行。 然而,最终的延续是基于任务的,因此始终会运行。 最后的延续处理第三个任务引发的异常。
我们建议你尽可能捕获最具体的异常。 如果你没有要捕获的特定异常,则可以忽略这个最终的基于任务的延续。 任何异常都将保持未处理状态,并且可以终止应用程序。
// eh-task-chain.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
int n = 1;
create_task([n]
{
wcout << L"In first task. n = ";
wcout << n << endl;
return n * 2;
}).then([](int n)
{
wcout << L"In second task. n = ";
wcout << n << endl;
return n * 2;
}).then([](int n)
{
wcout << L"In third task. n = ";
wcout << n << endl;
// This task throws.
throw exception();
// Not reached.
return n * 2;
}).then([](int n)
{
// This continuation is not run because the previous task throws.
wcout << L"In fourth task. n = ";
wcout << n << endl;
return n * 2;
}).then([](task<int> previousTask)
{
// This continuation is run because it is task-based.
try
{
// The call to task::get rethrows the exception.
wcout << L"In final task. result = ";
wcout << previousTask.get() << endl;
}
catch (const exception&)
{
wcout << L"<exception>" << endl;
}
}).wait();
}
/* Output:
In first task. n = 1
In second task. n = 2
In third task. n = 4
In final task. result = <exception>
*/
可以使用 concurrency::task_completion_event::set_exception 方法将异常与任务完成事件相关联。 任务并行性文档更详细地描述了 concurrency::task_completion_event 类。
concurrency::task_canceled 是一个重要的运行时异常类型,与 task 相关。 如果你调用 task::get 但该任务被取消,则运行时会引发 task_canceled。 相反,task::wait 会返回 task_status::canceled 并且不会引发,你可以通过基于任务的延续或在调用 task::get 时捕获并处理此异常。
切勿从代码中引发 task_canceled。 改为调用 concurrency::cancel_current_task。
如果任务发生异常并且该异常未被任务、其延续任务之一或主要应用捕获,则运行时将终止应用。 如果你的应用程序崩溃,你可以将 Visual Studio 配置为在引发 C++ 异常时中断。 诊断出未处理异常的位置后,使用基于任务的延续来处理它。