在 OpenMP* 和显式线程方法中选择 - Intel® Software Network

来源:百度文库 编辑:神马文学网 时间:2024/04/28 10:51:51
在 OpenMP* 和显式线程方法中选择
介绍
OpenMP 可提供一种功能强大、可移植的线程化应用的简单方法。但是有些情况下,开发商可能会因本地线程 API 的灵活性而选择使本地 API。本文中的指南有助于确定 OpenMP 是否适合于给定的环境。
作者:Andrew Binstock
该网站上的其他文章高度评价了使用 OpenMP 编程的种种优势,OpenMP是一个厂商中立的界面,可通过简单、可移植的方式对程序的各个部分进行线程处理。它由一组编译指示、API和环境变量组成,且适用于多种平台上的编译器。OpenMP 的最重要的特性是它给并行编程带来的可移植性和简单性。让我们快速浏览一个 C/C++编写的例子:
int j; #pragma omp parallel for for ( j = 0; j < ARRAY_SIZE; j++); array[j] +=j;
图 1. 借助 OpenMP 并行化简单的 for 循环。
刚好出现在 for 循环之前的 pragma语句会告知编译器生成线程代码,这些线程代码将执行以下操作:为运行时环境启动适当数量的线程、在这些线程中中断 for循环工作,以及等待线程完成,挂起正在运行的线程,并返回执行的原始线程。单个语句要完成相当多的工作,并且在执行过程中,开发商不必进行任何操作来创建或管理线程。
页面和feed选项
| |
收藏此页
|
目录
介绍
OpenMP 的优势
OpenMP 的限制
结论:如何选择?
作者简介
其他资源
下载 PDF(124KB)
给此页评分
 0 |  0
论坛标签
64bit china Gaming Intel intel web 2.0 TDK MID Mobile Game Guide Mobility Mobility Multi Core TDK Thread In Game -- Intel Thread Inner Investigation UMPC 论坛标签
给本页加标签
我的标签
站内标签
搜索标签


OpenMP 的优势
如果编译器未识别出编译指示语句,则将跳过该语句(按照 C 和 C++ 的 ANSI 标准)。因此,如果编译器不支持OpenMP,则包含这些编译指示的代码编译为单线程代码;如果编译器支持OpenMP,则包含这些编译指示的代码编译为多线程代码。请注意,进行线程处理时,OpenMP 不要求更改单线程代码。OpenMP仅添加编译指示形式的编译器指令。通过禁用 OpenMP,代码库会像以前一样进行编译和工作。(通过使用指令而非编译指示,OpenMP 支持Fortran 中的相同功能。有关 OpenMP 语法和操作的详细信息,请参阅openmp.org* 。)
研究过程序性能的开发商知道热点往往出现在循环内部,解决这些热点问题的最简单的方式之一是使用数据分解功能将循环的工作在多个线程中进行分配。这种简单有效的解决方案却受到显式线程 API(如 Win32* 或 UNIX/Linux*Pthreads)缺点的影响。具体而言,如何得知在运行时有多少线程可用呢?除非您的代码仅在指定的系统上运行,否则答案只能是不清楚。可以采用多种方式在运行时从系统中提取此信息,并动态创建适当数目的线程,但是该过程可能很麻烦,且在采用超线程(HT)技术时易于出错。
较简单的解决方案是让 OpenMP 算出正确的线程数目,并自动进行工作分配。在极少数情况下,开发商可能需要指定预先确定的要使用的线程数。这也可以在OpenMP 中通过使用环境变量或 API 调用实现。但是,OpenMP 专家不鼓励使用环境变量和API,因为它降低了原始程序的可移植性,并将关键因素置于程序控制之外。他们鼓励 OpenMP 用户尽可能使用编译指示。
图 1中的 for 循环是 OpenMP 可执行的规范示例。无法在此简单的语句中查看 OpenMP 的特定操作。for 循环的左花括号即为OpenMP 并行区域的开头:该区域依赖于 OpenMP 控制的多个线程。所有并行区域都在路障处结束。在这样的路障处,所有的 OpenMP线程都完成工作后,程序才暂停。这种暂停是非常重要的。在图 1 的示例中,您可能想在整个数组初始化完成后,才开始操作。
任何从并行代码到串行代码的转换都包含隐式路障。但是,有时多个循环同时工作,而您不希望循环之间出现路障。您希望将一个循环的线程可立即作为其后第二个并行循环的线程。可以使用 nowait 关键字来执行此操作,如第一个循环中使用的以下编译指示。
#pragma omp for nowait
关键字 nowait 表示,要完成的第一个线程将继续用于第二个循环,无需等待任何其他线程完成第一个循环。
当然,并不是所有可并行的工作都会出现在循环环境中。通常,程序将包含多个独立的任务,通过将各个任务分配给不同的线程可以并行执行这些任务。此设计称为功能分解,且 OpenMP 通过 sections 编译指示支持该设计:
#pragma omp sections
{
#pragma omp section
{
TaskA();
}
#pragma om section
{
TaskB();
}
#pragma omp section
{
TaskC();
}
}
图 2. 如何在 OpenMP 中并行化任务。
当代码段中仅执行唯一一个语句时,不需要编译指示后的花括号。如果有多个语句,则需要花括号。
OpenMP 遇到此代码时,会将每一项任务分配给最终执行它的线程。就像使用本地线程 API 一样,OpenMP 不能保证如何调度这些任务。首先可能会很好地执行 TaskC()。
曾使用过线程的开发商知道,只要两个或多个线程并行运行,就必须采取适当措施以防止某种并行编程问题的出现:阻止两个线程同时更新共享数据项(这种情况称为“数据竞跑”)。可以预测的是,OpenMP 能够满足此需求。
下面显示的编译指示标识了一次只能由一个线程执行的代码段:
#pragma omp critical
{
...some code here...
}
图 3. 锁定 OpenMP 中的代码段
关键字 critical暗示出现在本地 API(如 Pthreads 和Win32)中的关键区域观念。如果代码正在由一个线程运行,则任何需要执行该代码的其他线程都必须等到第一个线程到达右花括号为止。(请注意,在此处花括号起着重要的作用,正如其在图 1 和图 2 中的作用。花括号会告知 OpenMP 编译指示所覆盖的具体代码部分,这就是 OpenMP编译指示后面紧跟着单个语句(如图 1 所示)或左花括号(如图 2 和 3 所示)的原因所在。)
如上文所述,OpenMP 提供了一个重要的功能(由显式线程 API 提供)子集。但是,其高级实现要求 OpenMP 可处理符合特定预期效果的代码。如果代码不符合该指南,则 OpenMP 便不再是可选的解决方案。
OpenMP 的限制
并不是所有的循环都可以进行线程处理。例如,结果被同一循环的其他迭代使用的循环(这种情况称为“流依赖性”)将无法正常工作。此外,编译器和 OpenMP 代码无法检测出此情况,因此线程代码会生成错误的结果。图 4 提供了一个示例。
#pragma omp parallel for
for (i=2; i i++)
{
factorial[i]=i*factorial[i-1];
}
图 4. 因流依赖性而无法在 OpenMP 中工作的代码
OpenMP不会分析代码正确性,所以无法检测出此依赖性。因此,它将生成产生错误结果的代码。在许多情况下,流依赖性不大明显,但结果一样令人不快。同样,数据竞跑和其他线程问题可能会导致生成无法正常工作的代码。总之,OpenMP 要求开发商保证其代码的线程安全。
OpenMP 工作于粗粒度等级。它在对循环执行数据分解、将任务分配到各个线程以及其他高级操作等方面表现不俗。但是,如果代码需要执行错综复杂的线程操作,则 OpenMP 就不如本地 API 集合适了。
例如,假设一些线程向某个排列存放数据,而另外一些线程从该排列删除数据。该排列可用于存放从源中读取的数据,同时等待线程提取此数据并对其进行解析。如果解析是复杂且耗时的,而读取数据速度很快,可能需要使用该排列并指定多个线程进行解析,而指定几个线程进行读取。
该排列具有复杂的机制。当各个线程在排列中存放数据或删除数据时,就会发生许多锁定操作。如果排列已满,则线程输入必须等待;如果排列为空,则线程解析必须等待。这种基于其功能和特定变量状态的细粒度控制几乎不可能使用 OpenMP 来实现。
无法使用 OpenMP 的另一种情况是更改线程执行的优先级。所有本地线程 API 支持开发商通过给予更高优先级来指定某些线程获取更多系统资源,特别是执行时间。在 OpenMP 中,无法修改各个线程的优先级,因此这种控制等级不可用。
OpenMP 中还缺少不同本地线程 API 特有的几种特定功能。此处讨论了其中的两项功能。
在POSIX*线程支持中,有一个锁定构造(称为信号),该构造不仅能够锁定代码和解除锁定代码,还能实现更多功能。它支持多个线程锁定(或解除锁定)某个锁。特定的规则会应用于如何向等待信号的线程提供保护代码访问,而且某些应用可以有效地利用此计数方案。OpenMP 没有等效的构造。
在 Win32 线程 API 中,有一个线程处理选件称为光纤,通过此选件,用户可以编辑其自己的线程调度程序,从而对线程操作进行细粒度控制。这在 OpenMP 中也是无法实现的。
使用 OpenMP您还必须接受下面一点:它可以在后台执行许多线程处理工作。因此,您必须接受的是,您将无法了解所有实际执行的操作。实际上,OpenMP提供很少有关在后台所执行操作的信息。所以,如果需要执行其中的某一活动(如提高线程优先级),不能使用 OpenMP。
同理,如果程序在 OpenMP 下无法正常工作,您也无从了解究竟哪里出了问题。英特尔的线程工具可提供深入剖析,而几个 OpenMP API可为您提供其他信息以及在不同情况下测试代码的有限功能。除此之外,就没有什么可用的方法了。尽管如此,OpenMP实现在性能可靠性方面一直保持着良好的记录,因此,如果代码在 OpenMP 下无法正常运行,代码出现问题的几率远远大于 OpenMP。
结论:如何选择?
正如我们看到的,OpenMP 是一种强大、可移植且简便的程序线程处理方法。对于许多应用,它完全能够胜任。这类应用具备以下特征:
简单的数据分解
清晰的功能分解
简单的锁定和互斥需求
管理线程之间复杂交互的程序或依赖于线程函数密切处理的程序都需要使用本地线程 API。
请注意选择不具备排他性,这是非常重要的。许多程序同时使用 OpenMP 和本地线程 API。程序中与 OpenMP 线程具有共同点的部分使用OpenMP,而其他部分依赖于本地库。通过这种混合方法,可以方便地对各个模块进行线程处理,并使其具备更好的可移植性。
作者简介
Andrew Binstock 现任 Pacific Data Works LLC 首席分析师。他曾是普华永道《全球技术展望》的主编。他还为软件开发时报的企业整合专栏撰写文章。英特尔出版社目前已推出了他最近出版的新书,《使用超线程技术编程:如何为英特尔® IA-32处理器编写多线程软件》(Programming with Hyper-Threading Technology: How to WriteMultithreaded Software for Intel IA-32 Processors)。可以通过abinstock@pacificdataworks.com 与 Binstock 联系。