OpenMP介绍

OpenMP 是 Open MultiProcessing 的缩写。OpenMP 并不是一个简单的函数库,也不是一种独立的并行语言,而是为多处理器上编写并行程序而设计的指导共享内存、多线程并行的编译制导指令和应用程序编程接口。它提供了对并行算法的高层的抽象描述,程序员通过在源代码中加入专用的pragma来指明自己的意图,由此编译器可以自动将程序进行并行化,并在必要之处加入同步互斥以及通信。

OpenMP并行执行的程序要全部结束后才能执行后面的非并行部分的代码。这就是标准的并行模式fork/join式并行模式,其中fork创建新线程或者唤醒已有线程,join即多线程的会合,共享存储式并行程序就是使用fork/join式并行的。标准并行模式执行代码的基本思想是,程序开始时只有一个主线程,程序中的串行部分都由主线程执行,并行的部分是通过派生其他线程来执行,但是如果并行部分没有结束时是不会执行串行部分的。

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>
#include <omp.h>

int main(int argc, char const *argv[])
{
    #pragma omp parallel for
    for (int i = 0; i < 10; i++)
    {
        printf("i=%d\n", i);
    }
    return 0;
}

通过#pragma omp预处理指示符指定要采用OpenMP
通过#pragma omp parallel for来指定下方的for循环采用多线程执行,此时编译器会根据CPU的个数来创建线程数

编译程序gcc omp.c -fopenmp
输出如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ ./a.out
i=0
i=1
i=2
i=3
i=4
i=5
i=8
i=9
i=6
i=7

可见for循环中的语句被并行执行了

常用的库函数

omp_get_num_procs: 返回运行本线程的多处理机的处理器个数
omp_get_num_threads: 返回当前并行区域中的活动线程个数
omp_get_thread_num: 返回线程号
omp_set_num_threads: 设置并行执行代码时的线程个数
omp_init_lock: 初始化一个简单锁
omp_set_lock: 上锁操作
omp_unset_lock: 解锁操作,要和 omp_set_lock 函数配对使用
omp_destroy_lock: omp_init_lock 函数的配对操作函数,关闭一个锁

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <omp.h>

int main(int argc, char const *argv[])
{
    printf("CPU number: %d\n", omp_get_num_procs());

    printf("Parallel area A:\n");
    #pragma omp parallel
    {
        printf("Num of threads is: %d;This thread ID is %d\n", omp_get_num_threads(), omp_get_thread_num());
    }

    printf("Parallel area B:\n");
    omp_set_num_threads(100); 
    #pragma omp parallel
    {
        printf("Num of threads is: %d;This thread ID is %d\n", omp_get_num_threads(), omp_get_thread_num());
    }
    return 0;
}

OpenMP指令与子句

概述

C/C++中,OpenMP指令使用的格式为#pragma omp指令[子句[子句]...],OpenMP中的”指令“叫做 “编译指导语句”,后面的子句是可选的。例如:#pragma omp parallel private(i, j) parallel 就是指令,private是子句。

OpenMP的指令有以下一些:

  • parallel:用在一个代码段之前,表示这段代码将被多个线程并行执行
  • for: 用于 for 循环之前,将循环分配到多个线程中并行执行,必须保证每次循环之间无相关性
  • parallel for: parallel 和 for 语句的结合,也是用在一个 for 循环之前,表示 for 循环的代码将被多个线程并行执行
  • sections: 用在可能会被并行执行的代码段之前
  • parallel sections: parallel 和 sections 两个语句的结合
  • critical: 用在一段代码临界区之前
  • flush:保证各个 OpenMP 线程的数据影像的一致性
  • single: 用在一段叧被单个线程执行的代码段之前,表示后面的代码段将被单线程执行
  • barrier: 用于并行区内代码的线程同步,所有线程执行到 barrier 时要停止,直到所有线程都执行到 barrier 时才继续往下执行
  • atomic: 用于指定一块内存区域被制劢更新
  • master: 用于指定一段代码块由主线程执行
  • ordered: 用指定并行区域的循环按顺序执行
  • threadprivate: 用于指定一个变量是线程私有的

OpenMP 的子句有以下一些:

  • private: 指定每个线程都有它自己的变量私有副本
  • firstprivate: 指定每个线程都有它自己的变量私有副本,并且变量要被继承主线程中的初值
  • lastprivate: 主要是用来指定将线程中的私有变量的值在并行处理结束后复制回主线程中的对应变量
  • reduction: 用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的运算
  • nowait: 忽略指定中暗含的等待
  • num_threads: 指定线程的个数
  • schedule: 指定如何调度 for 循环迭代
  • shared: 指定一个或多个变量为多个线程间的共享变量
  • ordered: 用来指定 for 循环的执行要按顺序执行
  • copyprivate: 用于 single 指令中的指定变量为多个线程的共享变量
  • copyin: 用来指定一个 threadprivate 的变量的值要用主线程的值进行初始化
  • default: 用来指定并行处理区域内的变量的使用方式,缺省是 shared

parallel指令

parallel是用来构造一个并行块的,也可以使用其他指令如for、sections等和它配合使用

在C/C++中,parallel的使用方法如下:

1
2
3
4
#pragma omp parallel[for|sections][子句[子句]...]
{
    //code
}

代码示例:

1
2
3
4
5
6
7
8
int main(int argc, char const *argv[])
{
    #pragma omp parallel
    {
        printf("HelloWorld\n");
    }
    return 0;
}

程序将创建与机器核心数量相同的线程打印出HelloWorld
也可以指定使用多少个线程来执行,需要使用 num_threads 子句:

1
2
3
4
5
6
7
8
int main(int argc, char const *argv[])
{
    #pragma omp parallel num_threads(10)
    {
        printf("HelloWorld, ThreadID=%d\n", omp_get_thread_num());
    }
    return 0;
}

执行后将打印出以下结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ ./a.out
HelloWorld, ThreadID=1
HelloWorld, ThreadID=5
HelloWorld, ThreadID=6
HelloWorld, ThreadID=0
HelloWorld, ThreadID=9
HelloWorld, ThreadID=2
HelloWorld, ThreadID=7
HelloWorld, ThreadID=4
HelloWorld, ThreadID=3
HelloWorld, ThreadID=8

从ThreadId 的不同可以看出创建了 8 个线程来执行以上代码。所以 parallel 指令是用来为一段代码创建多个线程来执行它的。parallel 块中的每行代码都被多个线程重复执行。

for指令

for指令用来一个for循环分配到多线程中去执行。for 指令一般可以和 parallel 指令合起来形成 parallel for指令使用,也可以单独用在 parallel 语句的并行块中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int main(int argc, char const *argv[])
{
    // for 循环并行化声明形式1
    #pragma omp parallel
    {
        #pragma omp for
        for (int i = 0; i < 10; ++i){
            printf("%d", i);
        }
    }

    // for 循环并行化声明形式2
    #pragma omp parallel for
    for (int j = 0; j < 10; ++j){
        printf("%d", j);
    }
    return 0;
}

两种声明方式作用是一样的,但第二种更加简洁紧凑,但第一种声明形式可以在并行区域内、for循环以外写其他并行代码。

尽管OpenMP可以方便地对for循环进行并行化,但并不是所有的for循环都可以进行并行化。以下几种情况不能进行并行化:

  • for循环中的循环变量必须是有符号整形。例如,for (unsigned int i = 0; i < 10; ++i){}会编译不通过;
  • for循环中比较操作符必须是<, <=, >, >=。例如for (int i = 0; i != 10; ++i){}会编译不通过;
  • for循环中的第三个表达式,必须是整数的加减,并且加减的值必须是一个循环不变量。例如for (int i = 0; i != 10; i = i + 1){}会编译不通过;
  • 如果for循环中的比较操作为<或<=,那么循环变量只能增加;反之亦然。例如for (int i = 0; i != 10; –i)会编译不通过;
  • 循环必须是单入口、单出口,也就是说循环内部不允许能够达到循环以外的跳转语句,exit除外。异常的处理也必须在循环体内处理。例如:若循环体内的break或goto会跳转到循环体外,那么会编译不通过。

sections和section指令

section 语句是用在 sections 语句里用来将 sections 语句里的代码划分成几个不同的段,每段都并行执行。用法如下:

1
2
3
4
5
6
7
#pragma omp [parallel] sections [子句]
{
    #pragma omp section
    {
        //code
    }
}

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int main(int argc, char const *argv[])
{
    #pragma omp parallel sections
    {
        #pragma omp section
        printf("section 1 ThreadId = %d\n", omp_get_thread_num());
        #pragma omp section
        printf("section 2 ThreadId = %d\n", omp_get_thread_num());
        #pragma omp section
        printf("section 3 ThreadId = %d\n", omp_get_thread_num());
        #pragma omp section
        printf("section 4 ThreadId = %d\n", omp_get_thread_num());
    }
    return 0;
}

执行后将打印出以下结果:

1
2
3
4
5
$ ./a.out
section 1 ThreadId = 0
section 2 ThreadId = 1
section 3 ThreadId = 3
section 4 ThreadId = 2

从结果中可以发现各段代码没有按序执行,说明各个 section 里的代码都是并行执行的,并且各个 section 被分配到丌同的线程执行。 使用 section 语句时,需要注意的是这种方式需要保证各个 section 里的代码执行时间相差不大。

private子句

private 子句用亍将一个戒多个变量声明成线程私有的变量,变量声明成私有变量后,指定每个线程都有它自己的变量私有副本,其他线程无法访问私有副本。即使在并行区域外有同名的共享变量,共享变量在并行区域内不起任何作用,并且并行区域内不会操作到外面的共享变量。 private 子句的用法格式为:private(list)
代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int main(int argc, char const *argv[])
{
    int k = 100;
    #pragma omp parallel for private(k)
    for ( k = 0; k < 5; k++)
    {
        printf("k=%d\n", k);
    }
    printf("last k=%d\n", k);
    return 0;
}

执行后将打印出以下结果:

1
2
3
4
5
6
7
$ ./a.out
k=0
k=1
k=2
k=4
k=3
last k=100

因此可以看出,for 循环前的变量 k 和循环区域内的变量 k 其实是两个不同的变量。 用 private 子句声明的私有变量的初始值在并行区域的入口处是未定义的,它并不会继承同名共享变量的值,即无论该变量在并行区域外是否初始化,在进入并行区域后,该变量均不会初始化。

firstprivate和lastprivate子句

private 声明的私有变量不能继承同名变量的值,但实际情况中有时需要继承原有共享变量的值,OpenMP 提 供了 firstprivate 子句来实现这个功能,但并行区域的变量与共享变量仍然是两个不同的变量,并行区域结束后共享变量的值仍然没有改变。

有时在并行区域内的私有变量的值经过计算后,在退出并行区域时,需要将它的值赋给同名的共享变量,前面 的 private 和 firstprivate 子句在退出并行区域时都没有将私有变量的最后取值赋给对应的共享变量,lastprivate 子句就是用来实现在退出并行区域时将私有变量的值赋给共享变量。 代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int main(int argc, char const *argv[])
{
    int k = 100;
    #pragma omp parallel for firstprivate(k),lastprivate(k)
    for (int i = 0; i < 4; i++)
    {
        k += i;
        printf("k=%d\n",k);
    }
    printf("last k=%d\n", k);
    return 0;
}

执行后将打印出以下结果:

1
2
3
4
5
6
$ ./a.out
k=102
k=101
k=103
k=100
last k=103

OpenMP 规范中指出,如果是循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是 section 构造,那么是最后一个 section 语句中的值赋给对应的共享变量。注意这里说的最后一个 section 是指程序语法上 的最后一个,而不是实际运行时的最后一个运行完的。

threadprivate子句

threadprivate 子句用来指定全局的对象被各个线程各自复制了一个私有的拷贝,即各个线程具有各自私有的全局对象。 用法如下:
#pragma omp threadprivate(list)
threadprivate 和 private 的区别在于 threadprivate 声明的变量通常是全局范围内有效的,而 private 声明的 变量只在它所属的并行构造中有效。
threadprivate 的对应只能用于 copyin,copyprivate,schedule,num_threads 和 if 子句中,不能用于任 何其他子句中。

reduction子句

reduction 子句主要用来对一个或多个参数条目指定一个操作符,每个线程将创建参数条目的一个私有拷贝,在区域的结束处,将用私有拷贝的值通过指定的运行符运算,原始的参数条目被运算结果的值更新。 reduction 子句用法如下:
reduction(operator:list)

operator以及约定变量的初始值如下:

运算符 数据类型 默认初始值
+ 整数、浮点 0
- 整数、浮点 0
* 整数、浮点 1
& 整数 所有位均为1
| 整数 0
^ 整数 0
&& 整数 1
|| 整数 0

代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int main(){
    int sum = 0;
    printf("Before: %d\n", sum);

    #pragma omp parallel for reduction(+:sum)
    for (int i = 1; i <= 10; ++i){
        sum += i;
        printf("%d\n", sum);
    }

    printf("After: %d\n", sum);

    return 0;
}

执行后将打印出以下结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ ./a.out
Before: 0
1
3
6
4
9
15
9
19
7
15
After: 55

其中sum是共享的,采用reduction之后,每个线程根据reduction(+: sum)的声明算出自己的sum,然后再将每个线程的sum加起来。

如果在并行区域内不加锁保护就直接对共享变量迚行写操作,存在数据竞争问题,会导致不可预测的异 常结果。共享数据作为 private、firstprivate、lastprivate、threadprivate、reduction 子句的参数进入并行区域 后,就变成线程私有了,不需要加锁保护了。

shared子句

shared 子句用来声明一个或多个变量是共享变量。
用法如下:
shared(list) 在并行区域内使用共享变量时,如果存在写操作,必须对共享变量加以保护,否则不要轻易使 用共享变量,尽量将共享变量的访问转化为私有变量的访问。 循环迭代变量在循环构造区域里是私有的。声明在循环构造区域内的自动变量都是私有的。