65.9K
CodeProject 正在变化。 阅读更多。
Home

OpenMP入门指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (10投票s)

2010年2月20日

CPOL

9分钟阅读

viewsIcon

63042

本文旨在帮助任何初学者使用 OpenMP。

引言

有大量的技术文档阐述了并发和并行计算。代码可以在一个微处理器上并发运行,但如果没有多个核心或 CPU,代码将不会并行运行。在追求数据并行性的过程中,for 循环结构经常被强调,因为它是遍历数组元素等数据结构中每个项目的唯一控制流结构。在大多数情况下,这可以定义一个迭代空间,其边界被划分。这些加起来构成整体的独立部分在同一物理微处理器上的独立核心上同时执行。本文是为 OpenMP 的初学者准备的。Intel TBB 等线程库大量使用 C++ 语言,但正在进行大量研究,以使 OpenMP 更有效地与 C++ 配合使用。这可能涉及与标准模板库中定义的算法的某种集成。截至本文撰写之时,OpenMP 的当前版本是 3.0。

OpenMP 是利用 C 和 FORTRAN 语言实现并行化,进而实现并发的一种方式,尽管互联网上确实存在一些 C++ OpenMP 的源代码示例。OpenMP API 通常为这两种语言定义不同的内容,但它们有时也以相同的方式为某些元素定义内容。简而言之,其思想是找到可以通过 pragma 编译器指令进行并行化的代码块。其他循环结构最终会在满足某个条件或条件为布尔 true 时退出循环。OpenMP 结构定义为指令(pragma)加上一个代码块。它不能是任意代码块:它必须是结构化代码块。也就是说,一个代码块只有一个入口点在顶部,一个出口点在底部。因此,OpenMP 程序不能分支进入或分支出结构化代码块。OpenMP 的学习者在参考解释其工作原理的教程时,应不断查阅其官方网站 http://www.OpenMP.org。提供的代码示例是在命令行上使用 cl.exe 编译器和 /openmp 开关编译的。

定义

OpenMP 设计者的目标之一是使程序在不使用一个线程或多个线程时都能产生相同的结果。一个程序在执行一个线程或多个线程时产生相同结果的术语称为顺序等效。增量并行性指的是一种编程实践(并非总是可行),即一个顺序程序演变为一个并行程序。也就是说,程序员从上到下,逐块地处理顺序程序,并找到更适合并行执行的代码片段。因此,并行性是逐步添加的。话虽如此,OpenMP 是一个编译器指令、库例程和环境变量的集合,用于指定 FORTRAN、C 和(即将推出的)C++ 程序中的共享内存并发。请注意,在 Windows 操作系统中,任何可以共享的内存都会被共享。OpenMP 指令标记可以并行执行的代码(称为并行区域),并控制代码如何分配给线程。OpenMP 代码中的线程在分叉-合并模型下运行。当主线程在执行应用程序时遇到并行区域,会分叉出一个线程团队,这些线程开始执行并行区域内的代码。在并行区域结束时,团队内的线程会等待所有其他线程完成,然后才合并。主线程恢复执行并行区域之后的语句的串行执行。所有并行区域结束时的隐式屏障保留了顺序一致性。更具体地说,一个正在执行的 OpenMP 程序启动一个线程。在程序中需要并行执行的点,程序会分叉出额外的线程形成一个线程团队。线程在称为并行区域的代码区域中并行执行。并行区域结束时,线程会等待直到整个团队到达,然后它们会重新合并。此时,原始线程或主线程继续执行,直到下一个并行区域(或程序结束)。

所有 OpenMP pragmas 都具有相同的 ` #pragma omp` 前缀。后面跟着一个 OpenMP 指令结构和零个或多个可选的子句来修改该结构。OpenMP 是一种显式并行编程语言。编译器不会猜测如何利用并发。程序中表达的任何并行性都是因为程序员指示编译器将其放置在那里。要在 OpenMP 中创建线程,程序员会指定要并行运行的代码块。在 C 和 C++ 中,这通过用于在应用程序中定义并行区域的 pragma 来完成:使用 `parallel` 结构

#pragma omp parallel

现在,我们将通过几个简单的例子。编译后,此代码旨在将字符串打印到标准输出控制台。

#include <stdio.h>
int main()
{
   printf("E is the second vowel\n");
}

输出“E 是第二个元音”。

现在,我们添加编译器指令来定义此简单程序中的并行区域。

#include <stdio.h>
#include "omp.h"
int main()
{
#pragma omp parallel
  {
      printf("E is the second vowel\n");
  }
}

在双核处理器上,输出如下:

E is the second vowel
E is the second vowel

现在,我们包含一个局部变量。

#include <stdio.h>
#include "omp.h"
int main()
{
    int i=5;
#pragma omp parallel
  {
    printf("E is equal to %d\n",i);
  }
}

OpenMP 是一个共享内存编程模型。在大多数情况下,一个普遍的规则是,在并行区域之前分配的变量在线程之间是共享的。因此,程序打印:

E is equal to 5
E is equal to 5

OpenMP 规范包含一组环境变量和 API 函数,以实现对程序的控制。一个有用的环境变量是 `OMP_NUM_THREADS`,它将设置每个并行区域中团队使用的线程数。设置线程数的对应 API 函数是 `omp_num_threads()`。如果一个变量是在并行区域内声明的,那么它被认为是线程的局部变量或私有变量。在 C 中,变量声明可以出现在任何块中。示例如下。其中包含一个名为 `omp_get_thread_num()` 的函数调用。这个整数函数是 OpenMP 运行时库的一部分。它返回一个对每个线程唯一的整数,范围从零到线程数减一。

#include <stdio.h>
#include <omp.h>
int main()
{
  int i= 256; // a shared variable
#pragma omp parallel
  {
    int x; // a variable local or private to each thread
    x = omp_get_thread_num();
    printf("x = %d, i = %d\n",x,i);
  }
}

输出

x = 0, i = 256
x = 1, i = 256

//note the value of x decrements, 
//while the value of i remains the same

同步

同步就是关于时序。进程内的线程有时必须访问资源,因为容器进程创建了一个句柄表,线程可以通过句柄标识号访问资源。资源可以是注册表项、TCP 端口、文件或任何其他类型的系统资源。那些线程以有序的方式访问这些资源显然很重要。显然,两个线程不能同时在同一个 CRITICAL_REGION 中执行。例如,如果一个线程将某些数据写入消息队列,然后另一个线程覆盖了该数据,那么我们就发生了数据损坏。更具体地说,我们遇到了竞态条件:两个线程在同一时刻争先执行,因为它们(认为)被调度为那样。竞态条件会导致严重的系统崩溃。那么,OpenMP 如何处理这些问题呢?

OpenMP 具有同步构造,可确保对关键区域的互斥访问。当变量必须被所有线程共享,但在并行区域中必须对这些变量进行更新时,请使用这些构造。`critical` 构造就像一个锁,围绕着关键区域。一次只有一个线程可以执行受保护的关键区域。其他希望访问关键区域的线程必须等待,直到没有线程在执行关键区域。OpenMP 还具有 `atomic` 构造,以确保语句以原子、不可中断的方式执行。`atomic` 构造可用于的语句类型有限制,并且您只能保护单个语句。`single` 和 `master` 构造将控制并行区域内语句的执行,以便只有一个线程将执行这些语句(而不是允许一次只有一个线程)。前者将使用遇到的第一个线程,而后者将只允许主线程(在并行区域之外执行的线程)执行受保护的代码。

OpenMP 运行时库以编译器指令的形式表达,但有些语言特性只能通过库函数来处理。以下是其中一些:

  • `omp_set_num_threads()` 接受一个整数参数,并请求操作系统在后续的并行区域中提供该数量的线程。
  • `omp_get_num_threads()`(整数函数)返回当前线程团队中的实际线程数。
  • `omp_get_thread_num()`(整数函数)返回线程的 ID,ID 的范围从 0 到线程数减一。ID 为 0 的线程是主线程。

下面是使用一些 OpenMP API 函数来提取环境信息的代码。

#include <stdio.h>
#include <stdlib.h>
#include <omp.h>

int main (int argc, char *argv[]) 
{
int nthreads, tid, procs, maxt, inpar, dynamic, nested;

/* Start parallel region */
#pragma omp parallel private(nthreads, tid)
  {

  /* Obtain thread number */
  tid = omp_get_thread_num();

  /* Only master thread does this */
  if (tid == 0) 
    {
    printf("Thread %d getting environment info...\n", tid);

    /* Get environment information */
    procs = omp_get_num_procs();
    nthreads = omp_get_num_threads();
    maxt = omp_get_max_threads();
    inpar = omp_in_parallel();
    dynamic = omp_get_dynamic();
    nested = omp_get_nested();

    /* Print environment information */
    printf("Number of processors = %d\n", procs);
    printf("Number of threads = %d\n", nthreads);
    printf("Max threads = %d\n", maxt);
    printf("In parallel? = %d\n", inpar);
    printf("Dynamic threads enabled? = %d\n", dynamic);
    printf("Nested parallelism supported? = %d\n", nested);

    }

  }
}

输出

Thread 0 getting environment info...
Number of processors = 2
Number of threads = 2
Max threads = 2
In parallel? = 1
Dynamic threads enabled? = 0
Nested parallelism supported? = 0

更多核心概念

在某些情况下,循环中存在大量的独立操作。使用 OpenMP 的循环工作共享构造,您可以分割这些循环迭代并将它们分配给线程以进行并发执行。`parallel for` 构造将在 pragma 后面的单个 for 循环周围启动一个新的并行区域,并将循环迭代分配给团队的线程。完成分配的迭代后,线程会在并行区域结束时的隐式屏障处等待,然后与其他线程合并。可以将组合的 `parallel for` 构造拆分为两个 pragmas:一个 `parallel` 构造和一个 `for` 构造,`for` 构造必须在词法上包含在 `parallel` 区域内。下面是前者的一个例子。

#include <stdlib.h>
#include <stdio.h>
#include <omp.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; i++)
       c[i] = a[i] + b[i];
    printf("Thread %d: c[%d]= %f\n",tid,i,c[i]);
    }

  }  /* end of parallel section */

}

所以输出将是:

Number of threads = 2
 Thread 1 starting...
 Thread 0 starting...
 Thread 0: c[10]= 20.000000
 Thread 1: c[0]= 0.000000
 Thread 0: c[11]= 22.000000
 Thread 1: c[1]= 2.000000
 Thread 0: c[12]= 24.000000
 Thread 1: c[2]= 4.000000
Thread 0: c[13]= 26.000000
Thread 1: c[3]= 6.000000
Thread 0: c[14]= 28.000000
Thread 1: c[4]= 8.000000
Thread 0: c[15]= 30.000000
Thread 1: c[5]= 10.000000
Thread 0: c[16]= 32.000000
Thread 1: c[6]= 12.000000
Thread 0: c[17]= 34.000000
Thread 1: c[7]= 14.000000
Thread 0: c[18]= 36.000000
Thread 1: c[8]= 16.000000
Thread 0: c[19]= 38.000000
Thread 1: c[9]= 18.000000
Thread 0: c[20]= 40.000000
Thread 1: c[30]= 60.000000
Thread 0: c[21]= 42.000000
Thread 1: c[31]= 62.000000
Thread 0: c[22]= 44.000000
Thread 1: c[32]= 64.000000
Thread 0: c[23]= 46.000000
Thread 1: c[33]= 66.000000

.   .   .  .   .    .    .     

Thread 1: c[84]= 168.000000
Thread 1: c[85]= 170.000000
Thread 1: c[86]= 172.000000
Thread 1: c[87]= 174.000000
Thread 1: c[88]= 176.000000
Thread 1: c[89]= 178.000000
Thread 1: c[90]= 180.000000
Thread 1: c[91]= 182.000000
Thread 1: c[92]= 184.000000
Thread 1: c[93]= 186.000000
Thread 1: c[94]= 188.000000
Thread 1: c[95]= 190.000000
Thread 1: c[96]= 192.000000
Thread 1: c[97]= 194.000000
Thread 1: c[98]= 196.000000
Thread 1: c[99]= 198.000000
Thread 0: c[24]= 48.000000
Thread 0: c[25]= 50.000000
Thread 0: c[26]= 52.000000
Thread 0: c[27]= 54.000000
Thread 0: c[28]= 56.000000
Thread 0: c[29]= 58.000000

数据环境使用的子句

我们首先定义用于描述 OpenMP 中数据环境的术语。在程序中,变量是绑定到名称并保存值的容器(或者更具体地说,是内存中的存储位置)。变量可以在程序运行时被读取和写入(与只能读取的常量相反)。在 OpenMP 中,绑定到给定名称的变量取决于该名称出现在并行区域之前、并行区域内还是并行区域之后。当变量在并行区域之前声明时,它默认是共享的,并且该名称始终绑定到同一个变量。然而,OpenMP 包含可以添加到并行和工作共享构造中的子句,以控制数据环境。这些子句会影响绑定到名称的变量。`private (list)` 子句指示编译器为列表中的每个名称为每个线程创建一个私有(或局部)变量。私有列表中的名称必须已在并行区域之前定义并绑定到共享变量。这些新私有变量的初始值是未定义的,因此必须显式初始化。此外,在并行区域之后,绑定到出现在该区域的 `private` 子句中的名称的变量的值是未定义的。

#include <stdio.h>
#include <stdlib.h>
#include <omp.h>
#define N       50
#define CHUNKSIZE   5

int main (int argc, char *argv[]) 
{
  int i, chunk, tid;
  float a[N], b[N], c[N];
  char first_time;

/* Some initializations */
for (i=0; i < N; i++)
  a[i] = b[i] = i * 1.0;
chunk = CHUNKSIZE;
first_time = 'y';

#pragma omp parallel for     \
  shared(a,b,c,chunk)        \
  private(i,tid)             \
  schedule(static,chunk)     \
  firstprivate(first_time)

  for (i=0; i < N; i++)
  {
    if (first_time == 'y')
    {
      tid = omp_get_thread_num();
      first_time = 'n';
    }
    c[i] = a[i] + b[i];
    printf("tid= %d i= %d c[i]= %f\n", tid, i, c[i]);
  }
}

输出如预期:

tid= 0 i= 0 c[i]= 0.000000
tid= 1 i= 5 c[i]= 10.000000
tid= 0 i= 1 c[i]= 2.000000
tid= 1 i= 6 c[i]= 12.000000
tid= 0 i= 2 c[i]= 4.000000
tid= 1 i= 7 c[i]= 14.000000
tid= 0 i= 3 c[i]= 6.000000
tid= 1 i= 8 c[i]= 16.000000
tid= 0 i= 4 c[i]= 8.000000
tid= 1 i= 9 c[i]= 18.000000

.  . .   .  .  and so on

tid= 1 i= 47 c[i]= 94.000000
tid= 0 i= 43 c[i]= 86.000000
tid= 1 i= 48 c[i]= 96.000000
tid= 0 i= 44 c[i]= 88.000000
tid= 1 i= 49 c[i]= 98.000000
© . All rights reserved.