并行模式库PPL应用实战(一):使用task类创建并行任务
自 VS2010 起,微软就在 CRT 中集成了并发运行时(Concurrency Runtime),并行模式库(PPL,Parallel Patterns Library)是其中的一个重要组成部分。7 年过去了,似乎大家都不怎么Care这个事情,相关文章少少且多是蜻蜓点水。实际上这个库的设计相当精彩,胜过 C++ 标准库中 future/promise/async 系列许多,所以计划写一个系列探讨 PPL 在实际项目中应用中的各种细节。
好了,从最简单的代码开始,先演示下如何使用 task 类和 lambda 表达式创建一个并行任务:
// final_answer.cpp // compile with: /EHsc #include <ppltasks.h> #include <iostream> using namespace concurrency; using namespace std; int main(int argc, char *argv[]) { task<int> final_answer([] { return 42; }); cout << "The final answer is: " << final_answer.get() << endl; return 0; }
使用 Visual Studio 命令行工具编译
cl /EHsc final_answer.cpp
执行结果为:
[blockquote]
The final answer is: 42
[/blockquote]
task 类的原型如下:
template<typename _ReturnType> class task;
其模板参数 _ReturnType 是任务返回值类型。 task:get 方法则用于获取返回值,原型如下:
_ReturnType get() const;
task 类的构造函数原型:
template<typename T> __declspec(noinline) explicit task(T _Param);
可以看到这是个模板函数,其参数 _Param 可以是 lambda 表达式、函数对象、仿函数、函数指针等可以以 _Param()
形式调用的类型,或者 PPL 中的 task_completion_event<result_type> 类型。因此可以使用各种灵活的方式构造 task 对象,其中 lambda 表达式无疑是最方便常用的一种。
接下来我们修改上面的程序,打印出线程 id 以便观察并行任务的执行情况。
// final_answer_1.cpp // compile with: /EHsc #include <ppltasks.h> #include <iostream> #include <thread> using namespace concurrency; using namespace std; int main(int argc, char *argv[]) { cout << "Major thread id is: " << this_thread::get_id() << endl; task<int> final_answer([] { cout << "Thread id in task is:" << this_thread::get_id() << endl; return 42; }); cout << "The final answer is: " << final_answer.get() << endl; return 0; }
继续编译执行,得到输出结果:
[blockquote]
Major thread id is: 164824
Thread id in task is: 164824
The final answer is: 42
[/blockquote]
注意两个线程 id 是相同的,很有些意外,任务是在主线程执行的而非预计的其他后台工作线程。实际上这是 PPL 的优化策略造成的。
再修改下程序,在 task 对象构造完成后加一个 sleep 调用挂起当前线程一小段时间:
int main(int argc, char *argv[]) { cout << "Major thread id is: " << this_thread::get_id() << endl; task<int> final_answer([] { cout << "Thread id in task is:" << this_thread::get_id() << endl; return 42; }); this_thread::sleep_for(chrono::milliseconds(1)); cout << "The final answer is: " << final_answer.get() << endl; return 0; }
这次输出结果发生了变化:
[blockquote]
Major thread id is: 173404
Thread id in task is: 185936
The final answer is: 42
[/blockquote]
PPL 使用了一个新的线程执行并行任务,实际上 PPL 是使用了线程池来执行被调度到的任务。
而在上一个程序中,由于没有 sleep,也没有其他耗时的代码,执行到 task::get 方法时并行任务尚未被调度所以直接在当前线程执行该任务,这样就节省了两次线程切换的开销。
MSDN 中对 task::wait 方法的说明:
[blockquote]
It is possible for wait to execute the task inline, if all of the tasks dependencies are satisfied, and it has not already been picked up for execution by a background worker.
[/blockquote]
task::get 方法的内部实现会先调用 task::wait 方法所以有同样的效果。
本章小结:
1. task 类对象构造完成后即可被调度执行;
2. 并行有可能被优化在当前线程执行;
留一个问题,如果 task 对象构造后马上析构,该并行任务是否会被调度执行呢?
本章代码使用 visual studio community 2013 编译调试通过。
本章参考文档:
How to: Create a Task that Completes After a Delay task Class (Concurrency Runtime)