windows C++-有效使用PPL(三)
了解取消和异常处理如何影响对象销毁
在并行工作树中,取消的任务会阻止子任务运行。 如果一个子任务执行的操作对于应用程序很重要(如释放资源),则这可能会导致问题。 此外,任务取消可能导致异常通过对象析构函数进行传播,并在应用程序中导致不明确的行为。
在下面的示例中,Resource 类描述资源,Container 类描述保存资源的容器。 在其析构函数中,Container 类在它两个 Resource 成员上并行调用 cleanup 方法,然后在它第三个 Resource 成员上调用 cleanup 方法。
// parallel-resource-destruction.h
#pragma once
#include <ppl.h>
#include <sstream>
#include <iostream>
// Represents a resource.
class Resource
{
public:
Resource(const std::wstring& name)
: _name(name)
{
}
// Frees the resource.
void cleanup()
{
// Print a message as a placeholder.
std::wstringstream ss;
ss << _name << L": Freeing..." << std::endl;
std::wcout << ss.str();
}
private:
// The name of the resource.
std::wstring _name;
};
// Represents a container that holds resources.
class Container
{
public:
Container(const std::wstring& name)
: _name(name)
, _resource1(L"Resource 1")
, _resource2(L"Resource 2")
, _resource3(L"Resource 3")
{
}
~Container()
{
std::wstringstream ss;
ss << _name << L": Freeing resources..." << std::endl;
std::wcout << ss.str();
// For illustration, assume that cleanup for _resource1
// and _resource2 can happen concurrently, and that
// _resource3 must be freed after _resource1 and _resource2.
concurrency::parallel_invoke(
[this]() { _resource1.cleanup(); },
[this]() { _resource2.cleanup(); }
);
_resource3.cleanup();
}
private:
// The name of the container.
std::wstring _name;
// Resources.
Resource _resource1;
Resource _resource2;
Resource _resource3;
};
尽管这种模式在其自身上没有任何问题,但建议使用下面并行运行两个任务的代码。 第一个任务创建 Container 对象,第二个任务取消整个任务。 举例来说,该示例使用两个 concurrency::event 对象来确保在创建 Container 对象后发生取消,并在取消操作发生后销毁 Container 对象。
// parallel-resource-destruction.cpp
// compile with: /EHsc
#include "parallel-resource-destruction.h"
using namespace concurrency;
using namespace std;
static_assert(false, "This example illustrates a non-recommended practice.");
int main()
{
// Create a task_group that will run two tasks.
task_group tasks;
// Used to synchronize the tasks.
event e1, e2;
// Run two tasks. The first task creates a Container object. The second task
// cancels the overall task group. To illustrate the scenario where a child
// task is not run because its parent task is cancelled, the event objects
// ensure that the Container object is created before the overall task is
// cancelled and that the Container object is destroyed after the overall
// task is cancelled.
tasks.run([&tasks,&e1,&e2] {
// Create a Container object.
Container c(L"Container 1");
// Allow the second task to continue.
e2.set();
// Wait for the task to be cancelled.
e1.wait();
});
tasks.run([&tasks,&e1,&e2] {
// Wait for the first task to create the Container object.
e2.wait();
// Cancel the overall task.
tasks.cancel();
// Allow the first task to continue.
e1.set();
});
// Wait for the tasks to complete.
tasks.wait();
wcout << L"Exiting program..." << endl;
}
该示例产生下面的输出:
Container 1: Freeing resources...Exiting program...
此代码示例包含的以下问题可能导致其与预期行为不同:
- 父任务的取消会导致子任务(对 concurrency::parallel_invoke 的调用)也被取消。 因此,不会释放这两个资源;
- 父任务的取消将导致子任务引发内部异常。 由于 Container 析构函数不会处理此异常,因此异常会向上传播并且不会释放第三个资源;
- 子任务引发的异常通过 Container 析构函数进行传播。 从析构函数引发会将应用程序置于未定义的状态;
我们建议不在任务中执行关键操作(如释放资源),除非可以保证这些任务不会被取消。 我们还建议不使用可在类型的析构函数中引发的运行时功能。
不要在并行循环中反复阻止
并行循环(例如通过停滞操作进行控制的 concurrency::parallel_for 或 concurrency::parallel_for_each)可能导致运行时在很短的时间内创建许多线程。
当任务完成,或以协作方式停滞或让出时,并发运行时将执行其他工作。 当一个并行循环迭代停滞时,运行时可能会开始另一个迭代。 当不存在可用的空闲线程时,运行时将创建一个新线程。
当并行循环体偶尔停滞时,此机制可帮助最大化整体任务吞吐量。 但当多个迭代停滞时,运行时可能会创建多个线程来运行其他工作。 这可能导致内存不足的情况或较差的硬件资源使用情况。
请考虑以下示例,该示例在 parallel_for 循环的每次迭代中调用 concurrency::send 函数。 由于 send 以协作方式停滞,所以每次调用 send 时运行时都会创建一个新线程来运行其他工作。
// repeated-blocking.cpp
// compile with: /EHsc
#include <ppl.h>
#include <agents.h>
using namespace concurrency;
static_assert(false, "This example illustrates a non-recommended practice.");
int main()
{
// Create a message buffer.
overwrite_buffer<int> buffer;
// Repeatedly send data to the buffer in a parallel loop.
parallel_for(0, 1000, [&buffer](int i) {
// The send function blocks cooperatively.
// We discourage the use of repeated blocking in a parallel
// loop because it can cause the runtime to create
// a large number of threads over a short period of time.
send(buffer, i);
});
}
我们建议你重构代码来避免这种模式。 在此示例中,可通过在串行 for 循环中调用 send 来避免其他线程的创建。