并行模式库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)