高性能计算的线程模型:Pthreads 还是 OpenMP?

作者:Andrew Binstock


简介

UNIX 操作系统多年来一直支持线程,这是 UNIX 在服务器系统上异常活跃的主要原因之一。在过去几年间,Linux* 一直宣传自己通过改进线程的内核支持而在服务器上的出色表现。例如,kernel 最近发布的 2.6 版增加了全新的调度程序,能够通过可以在 Linux 系统上切换的线程来大幅度优化速度。kernel 之前的版本(2.4 版--Linux kernel 使用偶数代表发布的版本,奇数代表正在开发的版本)同样根据线程能力的大幅度改进来划分。这些进步有助于将 Linux 放在服务器上和放入支持高性能计算(HPC)的站点中。此外,Linux 放弃其原有的线程 API(称为 Linux 线程)并采用 Pthreads 作为其固有的线程界面,连接目前可用的大部分 UNIX 变量。

然而,Linux 开发人员(如使用 UNIX 和 Windows* 的程序员)可以使用称为 OpenMP* 的第二种线程 API,这由服务器厂商联盟精心设计而成。本文将 Pthreads 和 OpenMP 进行比较,并尝试确定哪一种能够使开发人员受益最多。


Pthreads 是什么?

Pthreads 是 IEEE(电子和电气工程师协会)委员会开发的一组线程接口,负责指定便携式操作系统接口(POSIX)。Pthreads 中的 P 表示 POSIX,实际上,Pthreads 有时候也代表 POSIX 线程。基本上,POSIX 委员会定义了一系列基本功能和数据结构,它希望能够被大量厂商采用,因此线程代码能够轻松地在操作系统上移植。委员会的梦想由 UNIX 厂商实现了,他们都大规模实施 Pthreads。(最著名的例外就是 Sun,它继续采用 Solaris* 线程作为其主要线程 API。)由于 Linux 的采用和移植到 Windows 平台,Pthreads 的可能性一直被进一步扩展。

Pthreads 指定 API 来处理线程要求的大部分行为。这些行为包括创建和终止线程、等待线程完成、以及管理线程之间的交互。后面的目录中存在各种锁定机制,能够阻止两个线程同时尝试修改相同的数据值:互斥体、条件变量和信号量。(从技术上讲,信号量不是 Pthreads 的一部分,但是它们从概念上更接近于与线程合作,而且可用于 Pthreads 能够运行的所有系统上。)

为了使用 Pthreads,开发人员必须为这一 API 专门编写代码。这就意味着它们必须包括标头文件、宣布 Pthreads 数据结构、并调用 Pthreads 指定的函数。基本上,此流程与使用其它库没有不同。和 UNIX 以及 Linux 上的其它库一样,Pthreads 库只是简单地链接到应用代码(通过 -lpthread 参数)。

虽然 Pthreads 库相当复杂(尽管不像一些其它固有的 API 设置那样广泛)而且显然具有便携性,但是全部固有线程 API 常用的严格限制条件也使它非常艰难:它需要大量线程专用代码。换言之,为 Pthreads 进行编码就要在线程模型中建立代码库,这是不可回避的。此外,一些决策(如需要使用的线程数据)也将成为程序中的硬编码。作为这些限制的交换条件,Pthreads 能提供对于线程操作的广泛控制--这是一个固有的低级 API,通常要求多个步骤来执行简单的线程任务。例如,使用线程循环来通过大型数据块需要宣布线程结构、单独创建线程、计算通向每个线程的循环并分配到线程、最终处理线程终止--所有这些必须由开发人员进行编码。如果循环不仅仅是简单的叠代,则线程指定代码的数量将显著增加。为了公平起见,对于如此多代码的需求存在于所有本地线程 API 中,而不仅仅是 Pthreads。

鉴于需要执行直接操作的线程代码的数量,开发人员一直在寻找更简单的 Pthreads 替代品。


OpenMP 是什么?

1997 年,一些厂商携手合作,在硬件制造商 Silicon Graphics 的支持下组成了新的线程接口。他们共同的问题是主要的时间操作系统全部利用了完全不同的线程编程方法。UNIX 使用 Pthreads、Sun 使用 Solaris 线程、Windows 使用自己的 API、而 Linux 则使用 Linux 线程(直到其后来采用 Pthreads)。委员会希望设计出能够支持代码库不需要改变即可在 Windows 和 UNIX/Linux 上平等运行的 API。1998 年,它提供了第一个称为 OpenMP 的 API 规范(在这些日子里,“开放”这个词汇与多厂商支持的概念相关,是指开放系统,而不是现在的开放源代码的含义。)

OpenMP 规范包括 API、一组编译指示、以及对 OpenMP 指定环境变量的几种设置。随着对标准的进一步修订,OpenMP 最实用的特性之一就是那一组编译指示,这一点逐渐明朗。通过明智地使用这些编译指示,单线程程序不需要向 API 或环境变量求助即可实现多线程。通过 OpenMP 2.0 的最新版本,OpenMP 架构审核委员会(ARB)是提出 OpenMP 规范的委员会的正式名称,显然开发人员使用编译指示而不是 API 作为优先选择。让我们来更深入地审视这种方法,从翻新编译指示开始。

以下关于编译指示的定义(摘自微软的文档)是最清晰的解释之一:“#pragma 指示为每个编译器提供了一种方法,能够提供机器指定和操作系统指定特性,同时保留与 C 和 C++ 语言的整体兼容性。根据定义,编译指示是机器指定或操作系统指定的,通常所有的编译器都不同。编译指示最常用于 C 和 C++ 中,格式如下:#pragma token-string

编译指示的主要方面是如果编译器不能识别指定的编译指示,则它必须忽略(根据 ANSI C 和 C++ 标准)。因此,将库指定的编译指示放入代码中是安全的,不需要担心如果采用不同的工具包进行编译会将代码破坏。

图 1 显示了简单的 OpenMP 编译指示行动。

#pragma omp parallel for 

for ( i = 0; i < x; i++ ) 

{

printf ( "Loop number is %d%d%d

",

i, i, i );
 
}

 

图 1. 通过简单的 OpenMP 编译指示实现循环的线程化

这种编译指示告诉编译器:以后的for 循环应该实现多线程化,而且线程应该并行执行。简单的解释就是:在编译器完成的工作与 OpenMP 库之间,for 循环将使用大量线程来执行。OpenMP 将致力于创建线程、通过在线程间划分交互作用来实现 for 循环的线程化、以及在 for 循环完成后处理线程。

虽然 OpenMP 不保证推理将创建多少线程,但是它通常选择等于可用执行管线数量的数量。在标准多处理器环境中,这一数量就是处理器的数量。在采用含超线程(HT)技术的处理器的系统上,管线的数量是处理器数量的两倍。API 函数或环境变量可用于撤销默认的线程数量。

OpenMP 将提供大量其它的编译指示来识别需要进行线程化的代码块、在线程中共享的范围变量或本地化到单独线程,到同步线程,如何安排任务或循环叠代到线程,等等。因此,最终它将通过线程功能提供中等级别纹理型控制。这种级别的纹理对于许多高性能计算(HPC)应用而言已经足够,在便携性和最佳执行能力的前提下,OpenMP 能够提供比大多数其它选择更好的选择,尤其是最大限度减少对于代码库的干扰。


哪一种线程模型适合您?

OpenMP 非常方便,因为它不会将软件锁定在事先设定的线程数量中。此类锁定工作给使用低级 API(如 Pthreads 或 Win32)的线程应用提出了一个大问题。当运行在更多处理器可用的平台上时,使用这些 API 编写的软件如何扩充线程的数量?一种方法一直是使用线程池(threading pool),其中在程序启动时创建一束线程,将工作分配到线程上。然而,这种方法需要相当多的线程指定代码,而且不能保证能够随着可用处理器的数量而合理地进行扩充。通过 OpenMP,不需要指定数量。

OpenMP 的编译指示还有另一项重要优势:通过禁用 OpenMP 支持,代码可用作为单一线程应用进行编译。当调试程序时,以这样的方式编译代码拥有巨大优势。如果没有这种选择,开发人员会经常发现很难说明复杂的代码是否能够正确工作,因为线程问题或因为与线程无关的设计错误。

如果开发人员需要精细纹理的控制,则他们可以使用 OpenMP 的线程 API。其中包括一小组函数,分为以下三个领域:查询执行环境的线程资源并设置当前的线程数量;设置、管理并释放锁来解决线程之间的资源访问;和一个小型的定时接口。使用这种 API 让人失望,因为它会取走纯编译指令方法提供的优势。在这个级别上,OpenMP API 是 Pthreads 提供的功能的一个小子集。这两个 API 都具有便携性,但是 Pthreads 能提供更大范围的原函数(primitive function),从而对线程化操作提供精细纹理的控制。因此,在必须单独管理线程的应用中,Pthreads 或本地的线程化 API(如 Windows 上的 Win32)将是更加自然的选择。

为了运行 OpenMP,开发人员必须促使编译器支持标准。在 Linux 和 Windows 上,面向 C/C++ 和 Fortran 的英特尔® 编译器支持 OpenMP。在 UNIX 平台上,SGI、Sun、惠普和 IBM 都提供兼容 OpenMP 的编译器。开放源代码版本通过 Omni OpenMP Compiler 项目提供,网址为http://www.hpcs.cs.tsukuba.ac.jp/omni-compiler/*。

因此,如果您正在为高性能计算(HPC)编写 UNIX 或 Linux 应用,则应该关注 Pthreads 和 OpenMP。您可能会发现 OpenMP 是一款不错的解决方案。


资源

Dave Butenhof,《POSIX 线程编程》 (Addison-Wesley, 1997年)。

Rich Gerber 和 Andrew Binstock,《超线程技术编程》(英特尔出版社,2004 年)。

Bil Lewis,《多线程编程教育》
http://www.lambdacs.com/*

Pthreads Win32,http://sources.redhat.com/pthreads-win32/*

开始使用 OpenMP*

OpenMP* 高级编程

英特尔® 线程处理工具和 OpenMP*


作者简介

Andrew Binstock 现任 Pacific Data Works LLC 首席分析师。此前他曾担任普华永道(PricewaterhouseCoopers)公司的高级技术分析师,以及《UNIX Review》《C Gazette》的前任主编。他是《Practical Algorithms for Programmers(程序员实用算法)》(Addison-Wesley Longman 出版)的主要作者,该书目前已是第 12 次印刷,广泛应用于美国 30 多个计算机科学部门。

 


有关编译器优化的更完整信息,请参阅优化通知