用Intel线程构建块进行安全、可伸缩性的并行编程
如果你也是今天众多编写多线程程序、利用多核计算平台的程序员之一,说不定你已经了解C++ STL中的容器类并不是线程友好的(即不太适用于多线程);如今,硬件行业的先行者Intel却提供了一款线程安全的C++模板库,来看看有没有你想要的吧。
多线程程序是出了名的难写、难测试、难调试,然而,我们又很需要多核台式及笔记本电脑所带来的性能优势,由此,开发者就不得不对他们的程序进行线程化改造。而多线程程序的开发又没有什么灵丹妙药,所以,有效地利用现有的库及工具能在一定程度上减轻由此带来的负担,Intel线程构建块(Intel Threading Building Blocks: Intel TBB)就是其中一款优秀的并行容器类,这款专门设计的C++模板库旨在辅助开发多线程程序,并可在程序中安全地添加可伸缩性并行特性。
你的容器类是线程安全的吗?
许多开发者依靠手工编写容器类或使用由C++标准模板库(STL)实现的容器类,可惜的是,这些库大多数都不是线程安全的,具体来说,当在多线程代码中使用STL时,STL规范中并未提及线程或容器类的行为,因此,这些STL容器类的实现一般都不是线程安全的。口说无凭,下面请判断使用STL map<string, MyClass>之后的值:
即便与两个不同的key相关联的两个value在上述代码中被修改之后,大多数STL实现也并未对正确性提供任何保证,如果同时执行这些操作又未进行同步,很可能会破坏map,又因为未对线程安全性有任何要求,访问两个不同的map都有可能导致数据损坏。
当然了,也可以另一种方式实现STL map模板类来保证上述代码的线程安全,不巧的是,某些常用map操作的顺序并不是以一种线程友好的方式来实现的,此时,当每个单独的操作为线程安全时,代码中的顺序操作可能会导致不确定的结果,例如,如果两个线程操作了以下代码map中的同一元素:
由Thread 0执行的代码进行了两项操作:一是它调用了操作符 [ ] 以返回一个对“Key1”相联对象的引用,如果这个key不在map中,操作符 [ ] 会为与此key相联的MyClass类型对象分配空间;二是操作符 = 被调用以复制MyClass的临时实例到由引用指向的对象中。
所期望的结果是,要么“Key1”不在map中,要么它与MyClass()的一个实例成对出现。但是如果没有用户干预的同步,即便每个操作符本身是线程安全的,都会有可能出现其他的结果。由Thread 1调用的erase方法可能会在Thread 0中对操作符 [ ] 及 = 的调用之间出现,如果这样的话,Thread 0将会对一个已删除的对象调用 = 操作符,明显是不正确的行为。这种类型的多线程bug被称为赛跑状态,无意的行为取决于哪个线程先执行它的操作。
从上例可以看出,赛跑状态的棘手之处,在于行为的非确定性,也许代码在测试时,Thread 1的erase从未出现在Thread 0的两个操作之间,因此bug就保留了下来,并存在于最终软件之中,在意想不到的时候危害软件的运行。
要想在使用非线程友好的容器类时避免此类的bug,开发者不得不在每个容器类的使用之处都加上锁,一次只允许一个线程访问容器类,这种看上去有点笨拙的方法限制了程序中的并发性,且每处新增的代码了也加大了程序的复杂性,然而,这却是利用现有类库所必须付出的代价。
挑战者的出现:Intel TBB容器类
Intel TBB库基于运行时的并行编程模型,它不但提供了在C++中使用线程的方法,还提供了可伸缩的并行算法,如对map、queue、vector容器类的安全、可伸缩性的实现。这些模板类能直接用在用户线程代码中,或与库中的并行算法组合使用。
前面也讨论过,某些容器类的标准接口并不是天生就对线程不感冒的,因此,Intel TBB并不能只是简单地取代对方,而是依旧遵循了STL的精神,在那些需要保证线程安全的地方,提供了修改后的接口。
Intel TBB库中所有的并行容器类全由精心设计的锁实现,无论何时调用了其中的一个方法,只有方法涉及到的数据结构部分被锁定,而结构的其他部分则允许其他线程同时访问。
下面,来介绍一下concurrent_hash_map、concurrent_queue、concurrent_vector这三个模板类,并以一个例子来说明如果发现及替换不安全的容器类。
concurrent_hash_map< Key, T, HashCompare >
Intel TBB的模板类concurrent_hash_map类似于STL容器类中map,但却允许多个线程同时访问其元素,它的哈希表把类型Key的key映射为类型T的value,属性HashCompare定义了映射中使用的哈希函数,它用于确定两个key是否相等。
下面是使用string类型的例子:
struct my_hash_compare {
static size_t hash( const string& x ) {
size_t h = 0;
for( const char* s = x.c_str(); *s; ++s )
h = (h*17)^*s;
return h;
}
static bool equal( const string& x, const string& y ) {
return x==y;
};
concurrent_hash_map就像是一个类型std::pair<const Key,T>的元素集合,一般来说,访问元素的目的,要么就是更新,要么就是读取,而模板类concurrent_hash_map则分别通过accessor和const_accessor类支持这两种类型的访问,它们就像是智能指针,实现了STL map中所缺乏的原子性访问。
accessor类代表了更新(写)操作,只要它指向了某个元素,所有其他试图在表中查找这个key的动作都会被阻拦,直到accessor完成;const_accessor也基本上类似,只不过它代表的是只读访问。在同一时间,允许有多个const_accessor指向同一元素,这样可在元素频繁读取但鲜有更新的情况下,极大地提高并发性。
我们主要通过insert、find、erase方法来访问concurrent_hash_map的元素,find与inset方法接受一个accessor或const_accessor作为参数,至于用哪个,完全取决于想要更新还是只读concurrent_hash_map;一旦方法返回,访问会一直持续到accessor或const_acessor被销毁。Remove方法隐含了写操作,因此在删除key前,它会一直等待其他的访问操作完成。
下面是插入一个新元素到concurrent_hash_map中的例子:
concurrent_hash_map<string, MyClass, my_hash_compare> string_table;
void insert_into_string_table ( string &key, MyClass &m ) {
//创建一个accessor,它会像智能指针那样进行写操作
concurrent_hash_map<string, MyClass, my_hash_compare>::accessor a;
//调用insert来创建一个新元素,如果已经有元素存在,则返回一个现有元素
// accessor对对此元素加锁,让此线程进行独占性访问
string_table.insert( a, key );
//修改由值
a->second = m;
//“a”这个accessor会在超出范围销毁时,释放对元素的锁
}
concurrent_queue< Key, T, HashCompare >
Intel TBB的模板类concurrent_queue<T>通过类型T的value实现了一个并发队列,多个线程可以同时向队列中压入或弹出元素,默认情况下,queue是无限的,但也可设置最大容量。
通常来说,队列是先进先出的数据结构,且在单线程程序中,队列也是遵守这种严格的顺序模式的。但如果多个线程同时压入或弹出,那么就会失去“先”这个词的含义。Intel TBB模板类concurrent_queue保证了如果某个线程压入了两个值,而又一个线程弹出了这两个值,它们都会以压入时的同样顺序弹出,并没有对不同线程压入值的交互弹出进行任何限制。
并行队列常用于“生产者——消费者”模式的应用程序中,某个线程产生的数据是由另一个线程来使用的。为对这类程序提供灵活的支持,Intel TBB提供了阻塞及非阻塞版本的pop,方法pop_if_present为非阻塞,它试图弹出一个值,但如果队列为家,它立即返回;另一方面,方法pop会一直阻塞,直到找到一个可用值,并从队列中弹出它。
下面的例子通过concurrent_queue<int>,使用了阻塞的pop方法以在两个线程间通讯,Thread 1输出由Thread 0压入队列的每个值:
与大多数STL容器类不同,concurrent_queue::size_type为有符号整型,而不是无符号。这是因为concurrent_queue::size()定义为push(压入)操作次数减去pop(弹出)操作次数,如果弹出数大于压入数,那么size()则为负数,例如,如果一个concurrent_queue为空,且有n个未决弹出操作,size()返回-n,这就方便得知有多少个使用者在等待队列。
concurrent_vector< Key, T, HashCompare >
TBB中最后一个并行容器类为concurrent_vector模板类,concurrent_vector为一可动态增长的数组,当其并行增长时,还允许访问其中的元素。
concurrent_vector类为安全地并行增长定义了两个方法:grow_by及grow_to_at_least。方法grow_by(n)允许安全地追加n个连续的元素到向量中,并返回第一个所追加元素的下标,每个元素由T()初始化;方法grow_to_at_least(n)会把一个当前小于n个元素的向量增长到大小n。
下面的代码安全地把一个C字符串追加到一个共享的向量中:
void Append ( concurrent_vector<char>& vector, const char* string) {
size_t n = strlen(string) + 1;
memcpy( &vector[vector_grow_by(n)], string, n+1 );
}
方法size()返回向量中的元素个数,其可能包含方法grow_by与grow_to_at_least正在进行并行构造的元素,因此,在concurrent_vector正在增长时,可安全地使用iterator(迭代子),只要iterator不超出end()的当前值,然而,iterator还是有可能会引用到一个正在并行构造的元素,所以,当使用Intel TBB concurrent_vector时,对构造及访问进行同步是开发者的责任。
一个string_count例子
下面的代码使用了STL map<string,int>来计算数组Data中有多少个不同的字符串,并生成了多个Win32线程让每个线程处理其中一块数据。
方法CountOccurrences创建了线程,并把数组中需处理的一段元素传递给每个线程,方法tally进行计数任务,每个线程都会遍历分配给它的那部分数据,并递增STL map中相应的元素。示例中第30行输出计数花费的总时间及字符串的总数(这个总数应等N的数据集大小)。
00: const size_t N = 1000000;
01: static string Data[N];
02: typedef map<string,int> StringTable;
03: StringTable table;
04: DWORD WINAPI tally ( LPVOID arg ) {
05: pair<int, int> *range =
06: reinterpret_cast< pair<int,int> * >(arg);
07: for( int i=range->first; i < range->second; ++i )
08: table[Data[i]] += 1;
09: return 0;
10: }
11: static void CountOccurrences() {
12: HANDLE worker[8];
13: pair<int,int> range[8];
14: int g = static_cast<int>(ceil(static_cast<double>(N) / NThread));
15: tick_count t0 = tick_count::now();
16: for (int i = 0; i < NThread; i++) {
17: int start = i * g;
18: range[i].first = start;
29: range[i].second = (start + g) < N ? start + g : N;
20: worker[i] =
21: CreateThread( NULL, 0, tally, &range[i], 0, NULL );
22: }
23: WaitForMultipleObjects ( NThread, worker, TRUE, INFINITE );
24: tick_count t1 = tick_count::now();
25: int n = 0;
26: for( StringTable::iterator i=table.begin();
27: i!=table.end(); ++i )
28: n+=i->second;
39:
30: printf("total=%d time = %g/n",n,(t1-t0).seconds());
31: }
找出不安全的容器类使用之处
在Microsoft Visual Studio .NET 2005中编译以上例子,并在一台有四个Intel Xeon处理器的服务器上以四个线程执行,你会立即发现有地方出错了,由30行输出的总数在每次执行时都不同,且永远都不等于N(1000000),也许是赛跑状态或其他线程错误。
下面,使用了Intel Thread Checker来进行检测,它不但可以检查用户代码中潜在的数据赛跑、死锁及其他线程错误,还可以找出线程间的潜在依赖性,并把这些依赖性问题映射到源代码中具体的行。此例中所有的潜在依赖性问题都指向同一处地方,第08行,这也是线程访问STL map之处。
图1:Intel Thread Checker可定位依赖性,易于消除潜在的冲突。
对应的解决方案
图1清楚地表明了需要同步访问map,首先,我们使用了一个Win 32 Mutex或CRITICAL_SECTION对象来保证访问的正确,正如前面所说的,粗粒度(即人工的)的同步,是在使用非线程安全库时,唯一保证线程安全的方法。
Win32 Mutex是一种内核对象,它对不同的进程都可见,当用Mutex对象来保证对STL map访问的安全时,这会带来巨大的性能开销;另一方面,Win32 CRITICAL_SECTION是一种 轻量的、位于用户空间内及进程内的Mutex对象,所以它是一个更佳的选择。
下面是使用STL map时的同步版本及顺序实现版本的对比图。
图2:三者都没有性能优势
如果未同步,STL map表现出良好的执行性能,但由于并行访问导致了不正确的结果;与大家预料的一样,Win32 Mutex开销最大,所以性能最低;而粗粒度同步的CRITICAL_SECTION对象同样性能也不佳,也就是说,两个线程安全的实现都比原始顺序执行要花更多的时间,同时,为保证结果正确的同步,也在某些方面限制了程序的可扩展性。图3是用TBB concurrent_hash_map替换STL map之后的结果。
图3:Intel TBB concurrent_hash_map相比STL map提供了更好的性能
对比concurrent_hash_map所带来性能增长及未同步处理的STL map,就会明白,Intel TBB所提供的线程安全是要付出一定代价的,但与其他线程安全的实现不同的是,它在提供并行访问线程安全的基础上,仍然有不止一倍的速度提升,因此,TBB并行容器类提供了其他非安全容器类所不能提供的高性能。
现今,越来越多的开发者面临转移到多核系统的需要,而且这个数字还在不断增长,他们亟需可用的工具,使多线程程序的开发错误更少,TBB中的并行容器类就是应运而生的这样一种工具,它以安全、可伸缩的实现,使这个过渡的过程更加轻松,从而可避免使用C++ STL中那些非线程友好的容器类及它们带来的问题。

转载于:https://blog.51cto.com/no001/1314755

用Intel线程构建块进行安全、可伸缩性的并行编程相关推荐

  1. 学习 Intel 线程构建块开源库(TBB)

    原文转载于:https://blog.csdn.net/zhu2695/article/details/51247267 学习 Intel 线程构建块开源库 简介 我们发现了 POSIX 线程和基于 ...

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

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

  3. 面向.NET开发人员的Dapr- actors 构建块

    原文地址:https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/actors The actor m ...

  4. 新特性:英特尔® 线程构建模块 4.2

    作者:杜伟 英特尔® 线程构建模块 (Intel® TBB) 是最为人们熟知的一种 C++ 线程库,其最新的版本现已更新至 4.2. 与之前的 4.1 版本相比,更新后的版本提供了多个重要的新特性. ...

  5. Win32 系统线程信息块(TIB)浅析

    作者:Matt Pietrek 编译:VCKBASE 原文出处:May 1996 Under The Hood Windows 操作系统各个版本之间虽然核心部分差异很大,但它们都共享一个关键的系统数据 ...

  6. 用可组合的构建块丰富用户界面?谷歌提出「可解释性」的最新诠释

    本文转自雷克世界(ID:raicworld) 编译 | 嗯~阿童木呀 随着在神经网络领域不断取得新的发展成果,有一个相对应的需求也亟待解决,即能够对其决策进行解释,包括建立它们在现实世界中行为方式的置 ...

  7. CUDA入门(三) 初探线程与块

    在配置GPU时一般都看重其的架构,流处理器数,以及显存数. 以英伟达的GPU为例架构一般以科学家的名字来命名,如Fermi(费米),Kepler(开普勒),现在主流的Maxwell(麦克斯韦),Pas ...

  8. Gradle构建脚本概要之构建块

    为什么80%的码农都做不了架构师?>>>    每个Gradle构建都包含三个基本构建块:project,task和property.每个构建至少一个project,进而又包含一个或 ...

  9. JBoss 系列十七:使用JGroups构建块MessageDispatcher 构建群组通信应用

    内容概要 本部分说明JGroups构建块接口MessageDispatcher,具体提供一个简单示例来说明如何使用JGroups构建块MessageDispatcher 构建群组通信应用 示例描述 构 ...

  10. Dapr微服务应用开发系列5:发布订阅构建块

    题记:这篇介绍发布订阅构建块,这是对事件驱动架构设计的一种实现落地. 注:对于"Building Blocks"这个词组的翻译,我之前使用了"构件块",现在和官 ...

最新文章

  1. Android 3.0 r1中文API文档(104) —— ViewTreeObserver
  2. 水壶问题 算法导论8.4
  3. AndroidStudio_Android Studio项目中报Call requires API level 18 (current min is 16)---Android原生开发工作笔记232
  4. Linux 命令(12)—— wc 命令
  5. 20200524每日一句
  6. D.579 - ClockHands
  7. 示波器学习笔记(2)——模拟示波器
  8. 数字信号处理——时域采样和频域采样(matlab)
  9. 快门光圈感光度口诀_曝光补偿怎么调,快门光圈感光度口诀,深度解析曝光补偿...
  10. Linux下键盘测试工具
  11. 针对平层住宅的分布式无线组网方案(含万兆NAS)
  12. pywin32的一系列用法
  13. 有助睡眠的方法有哪些?睡不着,这些方法就能帮到你
  14. FXCM富汇官网:通过十个问题学习外汇知识
  15. Java输入三条边判断是否能组成三角形,若能构成则输出什么三角形
  16. python输出时怎么保留两位小数_python输出怎么保留两位小数-Python教程
  17. 电脑端使用Fiddler对手机APP进行抓包分析示范
  18. 分布式闭锁-redisson的闭锁应用
  19. 企业数字化转型“核心方法论”
  20. 中国石油大学浏览器 服务器系统,中国石油大学信息中心

热门文章

  1. [转]怎么查看端口占用情况?
  2. Magento报错之SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry for key 1
  3. c#使用私有构造方法
  4. 解决Mac系统finder卡顿转菊花的问题
  5. 数据结构学习---有序链表的合并
  6. 未来手机、电脑和网络将整合为一块
  7. ]flume高并发优化——(1)load_balance
  8. 阿里云破世界记录,王坚说新登月计划需十年,我看不用!
  9. xtrabackup备份mysql数据库
  10. Oracle Flash Storage System新版手册集