原文转载于:https://blog.csdn.net/zhu2695/article/details/51247267

学习 Intel 线程构建块开源库

简介

我们发现了 POSIX 线程和基于 Windows 的线程的一种强大替代,即 Intel 线程构建块,该构建块是专为并行编程而设计的基于 C++ 的框架。并行编程是未来的发展趋势,但是如何实现高性能的并行编程,从而有效地利用多核 CPU 呢?使用诸如 POSIX 线程这样的线程库当然也是一种选择,不过,最初引入 POSIX 线程框架时已经考虑到了 C 语言。这是一种非常低级别的方法,例如,如果您无法访问任何并发容器,那么就不能使用任何并发算法。为此,Intel 推出了 Intel® 线程构建块 (Intel TBB),一种用于并行编程的基于 C++ 语言的框架,它提供了大量有趣的特性,具有比线程更高程度的抽象。

下载和安装 Intel TBB 非常简单:解压后的目录层次结构与 UNIX® 系统类似,其中包括 include、bin、lib 和 doc 文件夹。出于本文的目的,我选择使用 tbb30_20110427oss 稳定版。

开始使用 Intel TBB

Intel TBB 提供了许多好处。您可以先关注以下几个特性:

使用 Intel TBB 需要具备许多先决条件。在开始之前,您应当满足以下要求:

虽然不是必须的,但是 C++0x 中的 lambda 功能对 Intel TBB 来说相当有用。

本文对 Intel TBB 的讨论是从创建和研究一些任务和同步原语(互斥体)开始的,然后了解如何使用并发容器和并行算法。最后介绍如何使用原子模板来实现无锁编程。

回页首

了解 Intel TBB 任务

Intel TBB 基于 任务 的概念。您需要定义自己的任务,这些任务是从 tbb::task 中派生的,并使用 tbb/task.h 进行声明。用户需要在自己的代码中重写纯虚拟方法 task* task::execute ( )。下面展示了每个 Intel TBB 任务的一些属性:

  • 当 Intel TBB 任务调度程序选择运行一些任务时,会调用该任务的 execute 方法。这是入口点。
  • execute 方法会返回一个 task*,告诉调度程序将要运行的下一个任务。如果它返回 NULL,那么调度程序可以自由选择下一个任务。
  • task::~task( ) 是虚拟的,不管用户任务占用了什么资源,都必须在这个析构函数 (destructor) 中释放。
  • 任务是通过调用 task::allocate_root( ) 来分配的。
  • 主任务通过调用 task::spawn_root_and_wait(task) 来完成任务的运行。

下面的 清单 1 展示了第一个任务以及调用它的方式:

清单 1. 创建第一个 Intel TBB 任务
#include "tbb/tbb.h"
#include <iostream>
using namespace tbb;
using namespace std;class first_task : public task { public: task* execute( ) { cout << "Hello World!\n";return NULL;}
};int main( )
{ task_scheduler_init init(task_scheduler_init::automatic);first_task& f1 = *new(tbb::task::allocate_root()) first_task( );tbb::task::spawn_root_and_wait(f1);
}

要运行 Intel TBB 程序,则必须正确地初始化任务调度程序。清单 1 中的调度程序的参数是自动的,它使调度程序能够自行决定线程的数量。当然,如果您想控制衍生线程的最大数量,则可以重写此行为。但是在生产代码中,除非您真正清楚自己的行为,否则最好由调度程序决定最佳的线程数量。

现在,您已经创建了自己的第一个任务,让我们使用 清单 1 中的 first_task 衍生更多子任务。清单 2 引入了一些新的概念:

  • Intel TBB 提供了一个名为 task_list 的容器,可以将它用作一个任务集合。
  • 每个父任务都使用 allocate_child 函数调用创建一个子任务。
  • 在衍生出任何子任务之前,父任务必须调用 set_ref_count。如果没有这么做,则会导致出现未定义的行为。如果打算衍生一些子任务,然后等待它们完成,那么 count 的值必须为子任务数 + 1;否则,count 会等于子任务的数量。稍后会详细介绍这一点。
  • 调用 spawn_and_wait_for_all 的目的可以从其名称中推断出来:它可以衍生子任务并等待所有子任务完成。

以下是相关代码:

清单 2. 创建多个子任务
#include "tbb/tbb.h"
#include <iostream>
using namespace tbb;
using namespace std;class first_task : public task { public: task* execute( ) { cout << "Hello World!\n";task_list list1; list1.push_back( *new( allocate_child() ) first_task( ) );list1.push_back( *new( allocate_child() ) first_task( ) );set_ref_count(3); // 2 (1 per child task) + 1 (for the wait) spawn_and_wait_for_all(list1);return NULL;}
};int main( )
{ first_task& f1 = *new(tbb::task::allocate_root()) first_task( );tbb::task::spawn_root_and_wait(f1);
}

那么,为什么 Intel TBB 要求显式设置 set_ref_count 呢?文档中指出这样做主要是出于性能考虑。在衍生子任务之前,必须始终为任务设置 ref 计数。参见 参考资料,获得更多内容的链接。

您还可以创建任务组。下面的代码创建了一个任务组,它衍生了两个任务并等待它们完成。task_group 的 run 方法具有以下签名:

template<typename Func> void run( const Func& f )

run 方法衍生了一个计算 f( ) 的任务,但是并没有阻塞调用任务,因此控制权会立即返回。要等待子任务完成,调用任务调用了 wait(参见清单 3)。

清单 3. 创建一个 task_group
#include "tbb/tbb.h"
#include <iostream>
using namespace tbb;
using namespace std;class say_hello( ) { const char* message;public: say_hello(const char* str) : message(str) {  }void operator( ) ( ) const { cout << message << endl;}
};int main( )
{ task_group tg;tg.run(say_hello("child 1")); // spawn task and returntg.run(say_hello("child 2")); // spawn another task and return tg.wait( ); // wait for tasks to complete
}

注意,task_group 的语法非常简洁 — 不需要对内存收集进行任何调用,因此在直接处理任务时,不需要对 ref 计数执行任何操作。使用 Intel TBB 任务可以完成许多事情。请参考 Intel TBB 文档,以获得更多的详细信息。接下来我们将探讨并发容器。

回页首

并发容器:vector

现在,让我们关注 Intel TBB 的并发容器之一:concurrent_vector。该容器在头文件 tbb/concurrent_vector.h 中进行声明,基本接口与 STL vector 类似:

template<typename T, class A = cache_aligned_allocator<T> >
class concurrent_vector;

可以将多个线程安全添加到 vector,无需进行任何显式锁定。根据 Intel TBB 手册的解释,concurrent_vector 提供了以下属性:

  • 它提供了对其元素的随机访问;索引从位置 0 开始。
  • 可以安全地增加并发数量,还可以同时添加多个线程。
  • 添加新的元素并不会影响现有索引或迭代器。

但是,实现并发性需要付出代价。与 STL 不同,STL 添加新的元素会涉及数据移动,而 concurrent_vector 不会移动数据。该容器包含一系列连续的内存片段。显然,这会增加容器开销。

对于 vector 的并发性,有三种方法可以使用:

  • push_back:在 vector 末端添加元素。
  • grow_by(N):向 concurrent_vector 添加类型为 T 的 N 个连续元素,并返回指向第一个附加元素的迭代器。每个元素通过 T ( ) 进行初始化。
  • grow_to_at_least(N):将 vector 的大小增加到 N(如果 vector 的当前大小小于 N)。

将一个字符串附加到 concurrent_vector 之后,如下所示:

void append( concurrent_vector<char>& cv, const char* str1 ) { size_t count = strlen(str1)+1; std::copy( str1, str1+count, cv.grow_by(count) );
}

回页首

使用 Intel TBB 提供的现成的并行算法

Intel TBB 的一大优点是它使您能够并行处理源代码的多个部分,无需了解如何创建和维护线程。最常见的并行算法是 parallel_for。请考虑下面的示例:

void serial_only (int* array, int size) { for (int count = 0; count < size; ++count)apply_transformation (array [count]);
}

现在,如果前面的代码片段中的 apply_transformation 例程没有出现异常,比如只对单个数组元素应用某些转换,那么您将可以顺利地将负载分配到多个 CPU 内核中。您需要使用 Intel TBB 库提供的以下两个类来完成此操作:blocked_range(来自 tbb/blocked_range.h)和parallel_for(来自 tbb/parallel_for.h)。

blocked_range 类用于创建对象,可向 parallel_for 提供迭代范围,因此需要创建类似 blocked_range (0, size) 的内容,并将它作为输入传递给 parallel_forparallel_for 所需的第二个和最后一个参数是一个类,要求如 清单 4 所示(从 parallel_for.h 头文件粘贴)。

清单 4. 对 parallel_for 的第二个参数的要求
/** \page parallel_for_body_req Requirements on parallel_for bodyClass \c Body implementing the concept of parallel_for body must define:- \code Body::Body( const Body& ); \endcode        Copy constructor- \code Body::~Body(); \endcode                             Destructor- \code void Body::operator()( Range& r ) const; \endcode   Function call operator applying the body to range \c r.
**/

该代码表示您需要使用 operator( ) 创建自己的类,其中 blocked_range 用作参数,并编写此前在 operator() 方法定义内部创建的序列 for循环。代码构造函数和析构函数应当是公共的,使用编译器提供的默认值。清单 5 显示了相关代码。

清单 5. 创建 parallel_for 的第二个函数
#include "tbb/blocked_range.h"
using namespace tbb;class apply_transform{  int* array;  public:  apply_transform (int* a): array(a) {}  void operator()( const blocked_range& r ) const {  for (int i=r.begin(); i!=r.end(); i++ ){  apply_transformation(array[i]);  }  }
};

现在,您已经成功创建了第二个对象,只需调用 parallel_for 即可,如 清单 6 所示。

清单 6. 使用 parallel_for 并行化循环
#include "tbb/blocked_range.h"
#include "tbb/parallel_for.h"
using namespace tbb;void do_parallel_the_tbb_way(int *array, int size) { parallel_for (blocked_range(0, size), apply_transform(array));
}

回页首

Intel TBB 中的其他并行算法

Intel TBB 提供了许多种并行算法,例如,parallel_reduce(在 tbb/parallel_reduce.h 中进行声明)。这一次不会对每个数组元素应用转化,而是计算所有元素的总和。下面是序列代码:

void serial_only (int* array, int size) { int sum = 0;for (int count = 0; count < size; ++count)sum += array [count]; return sum;
}

从理论上讲,以并行方式运行这些代码意味着每个控制线程都会对数组的某些部分进行求和,因此必须在某个位置使用 join 方法将这些合计值加起来。清单 7 展示了 Intel TBB 代码。

清单 7. 对数组元素求和的序列循环
#include "tbb/blocked_range.h"
#include "tbb/parallel_reduce.h"
using namespace tbb;float sum_with_parallel_reduce(int*array, int size) {summation_helper helper (array);       parallel_reduce (blocked_range<int> (0, size, 5), helper);return helper.sum;
}

在将数组划分为若干个子数组以便将它们用于每个线程时,您希望保留一些粒度(例如,每个线程负责对 N 个元素求和,而 N 不应该太大或太小)。这时需要使用第三个参数 blocked_range。Intel TBB 要求 summation_helper 类满足两个条件:它必须提供一个名为 join 的方法来添加部分合计值,还要提供一个包含特殊参数的构造函数(称为 splitting constructor)。清单 8 提供了相关代码:

清单 8. 使用 join 方法创建 summation_helper 类并划分构造函数
class summation_helper {int* partial_array;
public:int sum;void operator( )( const blocked_range<int>& r ) {for( int count=r.begin(); count!=r.end( ); ++count)sum += partial_array [count];}summation_helper (summation_helper & x, split): partial_array (x. partial_array), sum (0) {}summation_helper (int* array): partial_array (array), sum (0){}void join( const summation_helper & temp ) { sum += temp.sum;  // required method }
};

接下来,Intel TBB 会调用 splitting 构造函数(第二个参数称为 split,是 Intel TBB 要求提供的伪参数),并使用一些元素来填充部分数组(这些元素的数量就是 blocked_range 中定义的粒度)。完成对子数组的求和操作后,join 方法将将这些结果相加。听上去有些复杂?乍一看也许是这样;但是请记住,您只需要三个方法:operator() 用于添加数组范围,join 用于添加部分结果,而 split 构造函数则用于启动新的 worker 线程。

Intel TBB 还提供了其他几个有用的算法,parallel_sort 是其中最有用的算法之一。参见 Intel TBB 参考手册(请参阅 参考资料),以获得有关的更多详细信息。

回页首

使用 Intel TBB 进行无锁编程

多线程编程过程中经常出现的一个问题是:锁定和解锁互斥体浪费了许多 CPU 周期。如果您了解 POSIX 线程,那么 Intel TBB 的 atomic 模板会令您大吃一惊。它的速度比互斥体快多了,而且您不再需要对代码进行锁定和解锁。atomic 可以解决所有编码问题吗?不,它的使用存在严格的限制;无论如何,如果您希望创建高性能代码,那么它非常有效。下面展示了如何将一个整数声明为 atomic 类型:

#include "tbb/atomic.h"
using namespace tbb;atomic<int> count;
atomic<float* > pointer_to_float;

现在,假设前面的可变计数可由多个控制线程访问。通常,您需要在执行写操作时对计数使用互斥体;然而,有了 atomic<int> 之后,您再也不需要这样做了。参见 清单 9。

清单 9. atomic fetch_and_add 不需要进行锁定
// writing with mutex, count is declared as int count;
{// … codepthread_mutex_lock (&lock);count += 1000; pthread_mutex_unlock (&lock);// … code continues
}// writing without mutex, count declared as atomic<int> count;
{// … codecount.fetch_and_add (1000); // no explicit locking/unlocking// … code continues
}

您没有使用 +=,而是使用了 atomic<T> 类的 fetch_and_add 方法。并且,它在 fetch_and_add 方法中没有使用任何互斥体。当执行fetch_and_add 时,会立刻向 count 增加 1000 个计数:要么所有线程都立即看到更新后的 count 值,要么所有线程都继续显示旧值。这就是将 count 声明为 atomic 变量的原因:对 count 的操作是原子性的,不会被进程或线程调度打断。不管线程是如何调度的,count 在不同的线程中不会出现不同的值。要深入讨论无锁编程,请参阅 参考资料。

atomic<T> 类提供了以下 5 个基本操作:

y = x; // atomic read
x = b; // atomic write
x.fetch_and_store(y); // y = x and return the old value of x
x.fetch_and_add(y); // x += y and return the old value of x
x.compare_and_swap(y, z); // if (x == z) x = y; in either case, return old value of x

此外,为了方便起见,还支持运算符 +=-=++ 和 --,但是都是在 fetch_and_add 之上实现的。如 tbb/atomic.h 所示,下面展示了这些运算符的定义方式(参见 清单 10)。

清单 10. 使用 fetch_and_add 定义的运算符 ++、--、+= 和 -=
value_type operator+=( D addend ) {return fetch_and_add(addend)+addend;
}value_type operator-=( D addend ) {// Additive inverse of addend computed using binary minus,// instead of unary minus, for sake of avoiding compiler warnings.return operator+=(D(0)-addend);
}value_type operator++() {return fetch_and_add(1)+1;
}value_type operator--() {return fetch_and_add(__TBB_MINUS_ONE(D))-1;
}value_type operator++(int) {return fetch_and_add(1);}value_type operator--(int) {return fetch_and_add(__TBB_MINUS_ONE(D));}

注意,atomic<T> 中的 T 类型只能是整数类型、枚举类型或指针类型。

结束语

本文篇幅有限,无法对 Intel TBB 库进行全面的描述。但是,Intel 的网站提供了这方面的大量文章,介绍了 Intel TBB 的各个方面。本文的目的只是简单介绍 Intel TBB 提供的一些有趣特性,比如任务、并发容器、算法,以及实现无锁代码的方式。希望本文的介绍能够激起您对 Intel TBB 的兴趣并使您成为其热心用户,就像本文的作者一样。

学习 Intel 线程构建块开源库(TBB)相关推荐

  1. 学习英特尔线程构建模块开源2.1库

    并行编程是未来,但是您如何才能有效利用多核CPU的高性能并行编程呢? 当然,也可以选择使用诸如POSIX线程之类的线程库,但是最初是出于C语言引入POSIX线程框架的. 这也是一种太底层的方法,例如, ...

  2. 收藏,7个学习Python编程的最佳开源库!

    点击上方"AI遇见机器学习",选择"星标"公众号 重磅干货,第一时间送达 仅做学术分享,如有侵权,联系删除 转载于 :机器学习算法与Python实战 很多伙伴们 ...

  3. Android好用的第三方开源库

    记录一些对工作学习有帮助的第三方开源库 快捷入口 音频类 AudioPlay Banner类 banner 流式布局 FlowLayout 网络请求框架 RxEasyHttp okhttp-RxHtt ...

  4. Procedural Landmass Generation开源库测评

    [博物纳新]是UWA旨在为开发者推荐新颖.易用.有趣的开源项目,帮助大家在项目研发之余发现世界上的热门项目.前沿技术或者令人惊叹的视觉效果,并探索将其应用到自己项目的可行性.很多时候,我们并不知道自己 ...

  5. 【转】DICOM医学图像处理:开源库mDCM与DCMTK的比較分析(一),JPEG无损压缩DCM图像

    转自:https://www.cnblogs.com/mfrbuaa/p/4004114.html 有修订 背景介绍: 近期项目需求,需要使用C#进行最新的UI和相关DICOM3.0医学图像模块的开发 ...

  6. android导入库项目,如何在android studio项目中导入开源库?

    导入Jar文件 这种可能很常见,可以下载到别人搞好的jar包,这样可以直接在自己的主module下创建libs文件夹(我这里这样,只是为了兼容eclipse方式),然后把jar文件放进去,然后在mod ...

  7. 在别的地方看的给程序员介绍一些C++开源库,记录给大家共同学习

    在别的地方看的<<给程序员介绍一些C++开源库>>,记录给大家共同学习 首先说明这篇文章不是出自我手,大家共同学习. 引用地址:http://oss.org.cn/?actio ...

  8. 【基础学习】GitHub 上100 的 Android 开源库分享

    本项目主要对目前 GitHub 上排名前 100 的 Android 开源库进行简单的介绍, 至于排名完全是根据GitHub搜索Java语言选择 (Best Match) 得到的结果, 然后过滤了跟A ...

  9. 深度学习(机器学习)模型压缩开源库整理

    最近由于项目要求,需要对模型进行压缩,查了一下都有 哪些开源出来的模型压缩开源库,然后看到原作者已经总结得挺好的, 值得学习!!! Tensorflow Lite:https://tensorflow ...

最新文章

  1. spring-aop入门
  2. Lambda 表达式到底有何用处?如何使用?
  3. CentOS LAMP一键安装网站环境及添加域名
  4. ActionScript3.0自定义Flex组件问题 重写组件的使用
  5. 操作系统:基于页面置换算法的缓存原理详解(下)
  6. 让 .Net 更方便的导入导出Excel
  7. [vue] 在组件中怎么访问到根实例?
  8. c语言中调试时go的作用,C语言调用GO
  9. 国外知名的开源项目托管网站
  10. centos so查看_照片信息查看器app安卓下载-照片信息查看器app下载v1.1.0 安卓版
  11. 数据治理需要注意什么问题
  12. Android开源项目推荐之「网络请求哪家强」
  13. spring 处理request.getInputStream()输入流只能读取一次问题
  14. python群聊机器人_基于python-wechaty的群聊助手机器人
  15. 密码领域专用语言 ZUC算法
  16. JS+MySQL获取 京东 省市区 地区
  17. python多找表格进行数据对比
  18. 第一个 Spark Steaming 程序
  19. macOS上的符号链接Symlink是什么,以及该怎么使用
  20. 色粉笔画的简史和怎样画色粉笔画?

热门文章

  1. presto获取上月月初和上月月末日期
  2. Js获取当前时间的月初月末
  3. 劲舞团服务器中断解决方法,劲舞团一直连接中断什么连锁反应
  4. MyBatis 的一级缓存和二级缓存
  5. 初探Orange PI 3
  6. Sigcomm‘2020 Annulus: A Dual Congestion Control Loop for Datacenter and WAN Traffic Aggregates论文阅读笔记
  7. Windows10安装Linux子系统(WSl2+Ubuntu20.04+图形界面)
  8. 我的html系统总结笔记
  9. ARM GIC 与Linux kernel的中断子系统(4)GIC和中断处理
  10. 【无标题】电信 HG680KA -Hi3798MV310 拆刷成功总结