全面了解并发的OpenMP API






4.41/5 (8投票s)
本文旨在通过使用 OpenMP 提供一个实现并发性的框架。
关于 OpenMP 的一些事实
本文不能替代 www.openmp.org 网站上的文档。该网站提供了 OpenMP API 当前版本的完整文档。在并行计算中,隐式线程的最强大用法是一种称为 OpenMP 的规范。虽然被认为是隐式线程库,但 OpenMP 或 Open Multi-Processing 是一个应用程序编程接口 (API),可用于显式地指导多线程、共享内存并行。OpenMP 不保证能最有效地利用共享内存,因此它本身并不适用于分布式内存并行系统。OpenMP 通过在源代码中插入特殊的 pragmas 和 directives 来实现并发,以指示要并发执行的段。这些 pragmas 被编译器识别和处理。这意味着 OpenMP 利用了多核处理器架构。简而言之,理解 OpenMP 对于有效利用这些新的多核处理器架构至关重要。
隐式线程库负责创建、管理和(在某种程度上)同步线程所需的许多复杂性。OpenMP 旨在适用于广泛的 SMP 架构,但它不是一种新编程语言。相反,它是可以添加到 FORTRAN、C 或 C++ 中的顺序程序中的表示法,用于描述工作如何在将在不同处理器或核心上执行的线程之间共享,并根据需要排序对共享数据的访问。在顺序程序中适当插入 OpenMP 功能,可以使许多(或许大多数)应用程序受益于共享内存并行架构,通常只需对代码进行少量修改。实际上,许多应用程序具有可利用的相当大的并行性。
OpenMP 的成功归因于多种因素。一个因素是它对结构化并行编程的强烈关注。另一个因素是 OpenMP 相对易于使用,因为处理并行程序细节的负担取决于编译器。它具有被广泛采用的主要优势,因此 OpenMP 应用程序可以在许多不同的平台上运行。但最重要的是,OpenMP 恰逢其时。随着小型和大型 SMP 以及其他多线程硬件部署的强劲增长,计算行业普遍认为需要一个易于学习和应用的共享内存编程标准。反过来,这意味着所有主要的编译器——用于 Windows 的 Microsoft Visual C/C++ .NET、用于 Linux 的 GNU GCC 编译器以及用于 Windows 和 Linux 的 Intel C/C++ 编译器,也都支持 OpenMP。
关于 OpenMP 如何工作的基本原理
OpenMP directives 标记可以并行执行的代码(称为并行区域)并控制代码如何分配给线程。OpenMP 代码中的线程遵循 fork-join 模型。当主线程在执行应用程序时遇到并行区域时,会生成一个线程团队,这些线程开始执行并行区域内的代码。在并行区域结束时,团队中的线程会等待直到团队中的所有其他线程完成,然后才“join”。主线程继续串行执行并行区域之后的语句。所有并行区域(以及 OpenMP 定义的大多数其他构造)末尾的隐式屏障保留了顺序一致性。
OpenMP 的 directives 让用户告诉编译器哪些指令要并行执行,以及如何将它们分配给将运行代码的线程。OpenMP directive 是一种特殊格式的指令,只有 OpenMP 编译器才能理解。事实上,它看起来像普通 FORTRAN 编译器的注释,或者 C/C++ 编译器的 pragma,因此如果编译器不了解 OpenMP,程序就可以像以前一样运行。该 API 没有很多不同的 directives,但当有效使用时它们非常强大。因此,粗略地说,OpenMP 实现了一种共享内存(或共享地址空间)编程模型。顾名思义,该模型假定程序将在共享部分或全部可用内存的一个或多个处理器上执行。共享内存程序通常由多个独立的线程(能够处理指令流的执行状态)执行;线程共享数据,但也可能拥有一些额外的私有数据。
除了正常的指令范围外,并行编程的共享内存方法还必须提供一种启动线程、分配工作给它们以及协调它们对共享数据的访问的手段,包括确保某些操作一次由一个线程执行。重申一下,OpenMP 通过在源代码中插入特殊的 pragmas 和 directives 来实现并发,以指示要并发执行的段。这些 pragmas 被编译器识别和处理。此 pragma 后面将跟着单个语句或用花括号括起来的代码块。当应用程序在执行期间遇到此语句时,它将 fork 一个线程团队,在每个线程上执行并行区域内的所有语句,并在区域中的最后一个语句之后 join 线程。
在许多应用程序中,大量独立的运算存在于循环中。使用 OpenMP 的循环工作共享构造,您可以拆分这些循环迭代并将它们分配给线程进行并发执行。并行 `for` 构造将在 pragmas 后面的单个 `for` 循环周围启动一个新的并行区域,并将循环迭代分配给团队中的线程。完成分配的迭代后,线程会停留在并行区域末尾的隐式屏障处,等待与其他线程 join。可以将组合的并行 `for` 构造拆分为两个 pragmas:一个并行构造和一个 `for` 构造,后者必须词法包含在并行区域内。当线程团队除了循环迭代之外还有并行工作时,使用此分离。您还可以将 `schedule` 子句附加到循环工作共享构造中,以控制迭代如何分配给线程。静态调度将在循环迭代开始执行之前将迭代分成块并分发给线程;如果有比线程更多的块,则使用循环调度。动态调度将为团队中的每个线程分配一个迭代块;当线程完成上一组迭代时,将分配一个新的块,直到所有块都分发完毕。静态和动态调度都有一个可选的 chunk 参数,用于控制每个块的迭代次数。那么,让我们看一个例子
#include <stdio.h>
#include <stdlib.h>
int main(argc,argv)
int argc; char *argv[];
{
double sum;
double a [256], b [256];
int status;
int n=256;
int i;
for ( i = 0; i < n; i++) {
a [i] = i * 0.5;
b [i] = i * 2.0;
}
sum = 0;
#pragma omp parallel for reduction(+:sum)
for (i = 1; i <= n; i++ ) {
sum = sum + a[i]*b[i];
}
printf ("sum = %f \n", sum);
}
编译:C:\..\..\>cl dot.c
输出
sum = 5559680.000000
在 OpenMP 下,所有数据默认都是共享的。在这种情况下,我们只需插入一个指令告诉编译器并行化循环,并将 `sum` 识别为归约变量,就可以并行化该循环。将循环迭代分配给线程、让不同线程构建部分和以及将它们累积到全局和的详细信息留给编译器处理。再说一遍,默认情况下,OpenMP 线程程序中的几乎所有变量都在线程之间共享。对共享访问规则的例外情况包括:与循环工作共享构造相关的循环索引变量(每个线程必须有自己的副本才能正确迭代分配的迭代集)、在并行区域内声明的变量或从并行区域调用的函数内声明的变量,以及任何其他放在线程堆栈上的变量(例如,函数参数)。如果您在 C/C++ 的循环工作共享构造中使用嵌套循环,则只有紧跟在构造后面的循环索引变量会自动设为私有。如果其他变量必须是线程私有的,例如嵌套循环的循环索引变量,请将 `private` 子句添加到相关构造中。将为每个线程分配列表中变量的本地副本。`private` 子句中列出的变量的初始值将是未定义的,您必须在使用区域中读取它们之前为它们赋值。
OpenMP 具有同步构造,可确保对关键区域的互斥。当变量必须由所有线程共享,但更新必须在并行区域中对这些变量执行时,请使用它们。`critical` 构造就像一个围绕关键区域的锁。一次只有一个线程可以执行受保护的关键区域内的代码。希望访问关键区域的其他线程必须等到没有线程正在执行关键区域。现在,因为我们说过,默认情况下 OpenMP 线程程序中的几乎所有变量都在线程之间共享(即,线程之间存在数据传递),我们可以回顾一下此规则的例外情况并加以利用,即共享访问规则有一个例外:与循环工作共享构造相关的循环索引变量。
此代码是循环工作共享的示例。在此示例中,循环的迭代在线程团队中动态调度。线程一次将执行 CHUNK 次迭代,然后再调度下一个 CHUNK 工作
#include <omp.h>
#include <sdio.h>
#include <stdlib.h>
#define CHUNKSIZE 10
#define N 100
int main (int argc, char *argv[])
{
int nthreads, tid, i, chunk;
float a[N], b[N], c[N];
/* Some initializations */
for (i=0; i < N; i++)
a[i] = b[i] = i * 1.0;
chunk = CHUNKSIZE;
#pragma omp parallel shared(a,b,c,nthreads,chunk) private(i,tid)
{
tid = omp_get_thread_num();
if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Number of threads = %d\n", nthreads);
}
printf("Thread %d starting...\n",tid);
#pragma omp for schedule(dynamic,chunk)
for (i=0; i<n; c[%d]="%f\n",tid,i,c[i]);")
c[i]="a[i]";
输出
Number of threads = 1
Thread 0 starting...
Thread 0: c[0]= 0.000000
Thread 0: c[1]= 2.000000
Thread 0: c[2]= 4.000000
Thread 0: c[3]= 6.000000
Thread 0: c[4]= 8.000000
Thread 0: c[5]= 10.000000
Thread 0: c[6]= 12.000000
Thread 0: c[7]= 14.000000
Thread 0: c[8]= 16.000000
Thread 0: c[9]= 18.000000
Thread 0: c[10]= 20.000000
Thread 0: c[11]= 22.000000
Thread 0: c[12]= 24.000000
Thread 0: c[13]= 26.000000
Thread 0: c[14]= 28.000000
Thread 0: c[15]= 30.000000
Thread 0: c[16]= 32.000000
Thread 0: c[17]= 34.000000
Thread 0: c[18]= 36.000000
Thread 0: c[19]= 38.000000
Thread 0: c[20]= 40.000000
. . . stopped here for brevity's sake..
下一个示例涉及组合并行循环归约。它演示了在组合并行循环构造内进行求和归约。请注意,假定默认数据元素作用域。没有指定共享或私有变量的子句。OpenMP 将自动使循环索引变量在团队内私有
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char *argv[])
{
int i, n;
float a[100], b[100], sum;
/* Some initializations */
n = 100;
for (i=0; i < n; i++)
a[i] = b[i] = i * 1.0;
sum = 0.0;
#pragma omp parallel for reduction(+:sum)
for (i=0; i < n; i++)
sum = sum + (a[i] * b[i]);
printf(" Sum = %f\n",sum);
}
输出
Sum = 328350.000000
一种常见的计算是将大量数据汇总或归约到一个单一值。例如,这可能包括数据项的总和或数据集的最大值或最小值。执行此类计算的算法依赖于用于收集部分和最终答案的共享变量。OpenMP 提供了一个子句来处理并发归约的细节。`reduction` 子句要求对组合数据进行关联和交换操作,以及一个归约变量列表。并行团队中的每个线程都会收到归约变量的私有副本,以在执行分配的计算时使用。与包含在 `private` 子句中的变量不同,这些私有变量使用取决于归约操作的值进行初始化。在带有 `reduction` 子句的区域结束时,所有本地副本将使用子句中指定的运算进行组合,并将结果存储在变量的共享副本中。因此,此时,让我们看一下一个数值计算程序,然后再看一个并行化版本。
在微积分中,当我们想要计算曲线下的面积时,我们会画出逼近曲线的矩形。曲线是连续的,这意味着它在每个点都有切线。也就是说,构成曲线的每一个点都有一个变化,一个方向的变化。这也称为微分系数。每个矩形都有一个中点,并且矩形有一个高度和一个宽度。矩形越窄,需要累加的矩形就越多。这被称为逼近面积。实际上,数值积分是一种计算函数曲线下方面积近似值的方法,尤其是在无法求解精确积分时。例如,常数 Pi 的值可以通过以下积分定义。然而,我们不直接求解这个积分,而是可以通过数值积分来近似求解。
下面示例中的代码是使用数值积分中点矩形法则求解上述积分的实现。为了计算曲线下方面积的近似值,我们必须计算一些矩形(`num_rects`)的面积,方法是找到每个矩形的中点(`mid`)并计算该矩形的高度(`height`),这只是函数在该中点的值。我们将所有矩形的高度(`sum`)加在一起,一旦计算出来,我们将高度之和乘以矩形的宽度(`width`)来确定所需总面积(`area`)的近似值以及 Pi 的值。这不是一个线程化应用程序,而是用于展示如何在我们编写第二个示例时对其进行线程化的代码。
#include <stdio.h>
static long num_rects=100000;
void main()
{
int i;
double mid, height, width, sum = 0.0;
double area;
width = 1.0/(double) num_rects;
for (i = 0; i < num_rects; i++){
mid = (i + 0.5) * width;
height = 4.0/(1.0 + mid*mid);
sum += height;
}
area = width * sum;
printf("Computed pi = %f\n",area);
}
计算出的 pi = 3.141593
第二个应用程序使用数值积分和中点矩形法则来计算 Pi 值的近似值,与第一个示例相同。该代码将积分范围分成 `num_rect` 个区间,并计算每个区间(矩形)中点的函数值 4.0/(1+x 2)。换句话说,从顺序程序创建 OpenMP 程序的第一个步骤是识别它包含的并行性。基本上,这意味着找到可以由不同处理器并发执行的指令、指令序列甚至大段代码。
#include <stdio.h>
#include <stdlib.h>
static long num_rects = 1000000;
int main(int argc, char* argv[])
{
double mid, height, width, sum=0.0;
int i;
double area;
width = 1./(double)num_rects;
#pragma omp parallel for private(mid, height) reduction(+:sum)
for (i=0; i < num_rects; i++) {
mid = (i + 0.5)*width;
height = 4.0/(1.+ mid*mid);
sum += height;
}
area = width * sum;
printf("The value of PI is %f\n",area);
}
Pi 的值为 3.141593。
已添加 OpenMP 循环工作共享构造。符合 OpenMP 标准的编译器将插入代码来生成一个线程团队,为每个线程提供 `mid`、`height` 和 `i` 变量的私有副本,将循环迭代分配给线程,最后,当线程完成分配的计算后,将 `sum` 的本地副本的值合并到共享版本中。此共享的 `sum` 副本将用于计算 Pi 近似值,当乘以区间宽度时。
最后,让我们检查一个矩阵乘法代码
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#define NRA 62 /* number of rows in matrix A */
#define NCA 15 /* number of columns in matrix A */
#define NCB 7 /* number of columns in matrix B */
int main (int argc, char *argv[])
{
int tid, nthreads, i, j, k, chunk;
double a[NRA][NCA], /* matrix A to be multiplied */
b[NCA][NCB], /* matrix B to be multiplied */
c[NRA][NCB]; /* result matrix C */
chunk = 10; /* set loop iteration chunk size */
/*** Spawn a parallel region explicitly scoping all variables ***/
#pragma omp parallel shared(a,b,c,nthreads,chunk) private(tid,i,j,k)
{
tid = omp_get_thread_num();
if (tid == 0)
{
nthreads = omp_get_num_threads();
printf("Starting matrix multiple example with %d threads\n",nthreads);
printf("Initializing matrices...\n");
}
/*** Initialize matrices ***/
#pragma omp for schedule (static, chunk)
for (i=0; i < NRA; i++)
for (j=0; j < NCA; j++)
a[i][j] = i + j;
#pragma omp for schedule (static, chunk)
for (i=0; i<NCA; i++)
for (j=0; j < NCB; j++)
c[i][j] = 0;
/*** Do matrix multiply sharing iterations on outer loop ***/
/*** Display who does which iterations for demonstration purposes ***/
printf("Thread %d starting matrix multiply...\n",tid);
#pragma omp for schedule (static, chunk)
for (i=0; i < NRA; i++) {
for (j=0; j < NCB; j++)
for (k = 0; k < NCA; k++)
c[i][j] += a[i][k] * b[k][j];
}
} // end of parallel region
//**Print Results**//
printf("******************************************************\n");
printf("Result Matrix:\n");
for (i=0; i < NRA; i++)
{
for (j = 0; j < NCB; j++)
printf("%6.2f ", c[i][j]);
printf("\n");
}
printf("******************************************************\n");
printf("Done\n");
}
输出
Starting matrix multiple example with 2 threads
Initializing matrices...
Thread 0 starting matrix multiply...
Thread 1 starting matrix multiply...
Thread=0 did row=0
Thread=1 did row=10
Thread=0 did row=1
Thread=1 did row=11
Thread=0 did row=2
Thread=1 did row=12
Thread=0 did row=3
Thread=1 did row=13
Thread=0 did row=4
Thread=1 did row=14
Thread=0 did row=5
Thread=1 did row=15
Thread=0 did row=6
Thread=1 did row=16
Thread=0 did row=7
Thread=1 did row=17
Thread=0 did row=8
Thread=1 did row=18
Thread=0 did row=9
Thread=1 did row=19
Thread=0 did row=20
Thread=1 did row=30
Thread=0 did row=21
Thread=1 did row=31
Thread=0 did row=22
Thread=1 did row=32
Thread=0 did row=23
Thread=1 did row=33
Thread=0 did row=24
Thread=1 did row=34
Thread=0 did row=25
Thread=1 did row=35
Thread=0 did row=26
Thread=1 did row=36
Thread=0 did row=27
Thread=1 did row=37
Thread=0 did row=28
Thread=1 did row=38
Thread=0 did row=29
Thread=1 did row=39
Thread=0 did row=40
Thread=1 did row=50
Thread=0 did row=41
Thread=1 did row=51
Thread=0 did row=42
Thread=1 did row=52
Thread=0 did row=43
Thread=1 did row=53
Thread=0 did row=44
Thread=1 did row=54
Thread=0 did row=45
Thread=1 did row=55
Thread=0 did row=46
Thread=1 did row=56
Thread=0 did row=47
Thread=1 did row=57
Thread=0 did row=48
Thread=1 did row=58
Thread=0 did row=49
Thread=1 did row=59
Thread=0 did row=60
Thread=0 did row=61
******************************************************
Result Matrix:
0.00 1015.00 2030.00 3045.00 4060.00 5075.00 6090.00
0.00 1120.00 2240.00 3360.00 4480.00 5600.00 6720.00
0.00 1225.00 2450.00 3675.00 4900.00 6125.00 7350.00
0.00 1330.00 2660.00 3990.00 5320.00 6650.00 7980.00
0.00 1435.00 2870.00 4305.00 5740.00 7175.00 8610.00
0.00 1540.00 3080.00 4620.00 6160.00 7700.00 9240.00
0.00 1645.00 3290.00 4935.00 6580.00 8225.00 9870.00
0.00 1750.00 3500.00 5250.00 7000.00 8750.00 10500.00
0.00 1855.00 3710.00 5565.00 7420.00 9275.00 11130.00
0.00 1960.00 3920.00 5880.00 7840.00 9800.00 11760.00
0.00 2065.00 4130.00 6195.00 8260.00 10325.00 12390.00
0.00 2170.00 4340.00 6510.00 8680.00 10850.00 13020.00
0.00 2275.00 4550.00 6825.00 9100.00 11375.00 13650.00
0.00 2380.00 4760.00 7140.00 9520.00 11900.00 14280.00
0.00 2485.00 4970.00 7455.00 9940.00 12425.00 14910.00
0.00 2590.00 5180.00 7770.00 10360.00 12950.00 15540.00
0.00 2695.00 5390.00 8085.00 10780.00 13475.00 16170.00
0.00 2800.00 5600.00 8400.00 11200.00 14000.00 16800.00
0.00 2905.00 5810.00 8715.00 11620.00 14525.00 17430.00
0.00 3010.00 6020.00 9030.00 12040.00 15050.00 18060.00
0.00 3115.00 6230.00 9345.00 12460.00 15575.00 18690.00
0.00 3220.00 6440.00 9660.00 12880.00 16100.00 19320.00
0.00 3325.00 6650.00 9975.00 13300.00 16625.00 19950.00
0.00 3430.00 6860.00 10290.00 13720.00 17150.00 20580.00
0.00 3535.00 7070.00 10605.00 14140.00 17675.00 21210.00
0.00 3640.00 7280.00 10920.00 14560.00 18200.00 21840.00
0.00 3745.00 7490.00 11235.00 14980.00 18725.00 22470.00
0.00 3850.00 7700.00 11550.00 15400.00 19250.00 23100.00
0.00 3955.00 7910.00 11865.00 15820.00 19775.00 23730.00
0.00 4060.00 8120.00 12180.00 16240.00 20300.00 24360.00
0.00 4165.00 8330.00 12495.00 16660.00 20825.00 24990.00
0.00 4270.00 8540.00 12810.00 17080.00 21350.00 25620.00
0.00 4375.00 8750.00 13125.00 17500.00 21875.00 26250.00
0.00 4480.00 8960.00 13440.00 17920.00 22400.00 26880.00
0.00 4585.00 9170.00 13755.00 18340.00 22925.00 27510.00
0.00 4690.00 9380.00 14070.00 18760.00 23450.00 28140.00
0.00 4795.00 9590.00 14385.00 19180.00 23975.00 28770.00
0.00 4900.00 9800.00 14700.00 19600.00 24500.00 29400.00
0.00 5005.00 10010.00 15015.00 20020.00 25025.00 30030.00
0.00 5110.00 10220.00 15330.00 20440.00 25550.00 30660.00
0.00 5215.00 10430.00 15645.00 20860.00 26075.00 31290.00
0.00 5320.00 10640.00 15960.00 21280.00 26600.00 31920.00
0.00 5425.00 10850.00 16275.00 21700.00 27125.00 32550.00
0.00 5530.00 11060.00 16590.00 22120.00 27650.00 33180.00
0.00 5635.00 11270.00 16905.00 22540.00 28175.00 33810.00
0.00 5740.00 11480.00 17220.00 22960.00 28700.00 34440.00
0.00 5845.00 11690.00 17535.00 23380.00 29225.00 35070.00
0.00 5950.00 11900.00 17850.00 23800.00 29750.00 35700.00
0.00 6055.00 12110.00 18165.00 24220.00 30275.00 36330.00
0.00 6160.00 12320.00 18480.00 24640.00 30800.00 36960.00
0.00 6265.00 12530.00 18795.00 25060.00 31325.00 37590.00
0.00 6370.00 12740.00 19110.00 25480.00 31850.00 38220.00
0.00 6475.00 12950.00 19425.00 25900.00 32375.00 38850.00
0.00 6580.00 13160.00 19740.00 26320.00 32900.00 39480.00
0.00 6685.00 13370.00 20055.00 26740.00 33425.00 40110.00
0.00 6790.00 13580.00 20370.00 27160.00 33950.00 40740.00
0.00 6895.00 13790.00 20685.00 27580.00 34475.00 41370.00
0.00 7000.00 14000.00 21000.00 28000.00 35000.00 42000.00
0.00 7105.00 14210.00 21315.00 28420.00 35525.00 42630.00
0.00 7210.00 14420.00 21630.00 28840.00 36050.00 43260.00
0.00 7315.00 14630.00 21945.00 29260.00 36575.00 43890.00
0.00 7420.00 14840.00 22260.00 29680.00 37100.00 44520.00
*************************************************************************
Done.
鉴于大型 SMP 和多线程计算机的趋势,创建共享内存并行程序的策略和技术变得广为人知至关重要。本文旨在解释如何结合 Fortran、C 和 C++ 等主要编程语言使用 OpenMP 编写此类并行程序。
参考文献
- 并发的艺术,作者:Clay Breshears