OpenMP教程

§OpenMP

OpenMP (Open Multi-Processing)是一套支持跨平台共享内存方式的多线程并发的编程API,使用C,C++和Fortran语言,可以在大多数的处理器体系和操作系统中运行。OpenMP包括一套编译器指令、库和一些能够影响运行行为的环境变量。OpenMP采用#pragma omp来控制线程,即使用c/c++标准留给编译器厂商的实现定义行为控制语法来实现,因此OpenMP标准针对的是编译器厂商。要确定某个平台是否支持OpenMP,只需要看是否有支持OpenMP的编译器,如windows的visual studio编译器只支持OpenMP 2.0,而最新版的OpenMP标准为OpenMP5.0,因此windows平台对OpenMP的支持不太友好。

§例1、向量加法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 文件:vector_add_omp.cpp
#include <iostream>
#include <vector>

int main() {
    const int n = 10;
    std::vector<int> v1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> v2 = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    std::vector<int> v3(n);

    #pragma omp parallel for
    for (int i = 0; i < n; ++i)
        v3[i] = v1[i] + v2[i];

    for (int i = 0; i < n; ++i)
        std::cout << v3[i] << " ";
    std::cout << std::endl;

    return 0;
}

编译:g++ -fopenmp vector_add_omp.cpp -o vector_add_omp

执行:

1
2
$ ./vector_add_omp
11 11 11 11 11 11 11 11 11 11

OpenMP采用fork/join并行模式,程序开始时只有一个主线程,程序中的串行部分由主线程执行,并行部分派生其它线程共同执行,所有线程执行完毕后才会退出并行区域,因此在输出v3时可以确保已经计算完成。

parallel for中循环小括号内的语句必须按照一定的规范书写才能并行:

(1) 第一条语句是循环变量初始化的语句,必须写成"变量=初值"的形式。如i=0。

(2) 第二条语句是循环限制语句,必须为这4种形式之一:i < n、 i <= n、 i > n、 i>= n。需要注意循环中不能有影响循环次数的语句,如break语句或者改变循环变量的语句。

(3) 第三条语句改变循环变量,必须写成这9种形式之一:i++、++i、i--、--i、i += inc、i -= inc、i = i + inc、i = i - inc、i = inc + i

此外,还需要注意的是,程序中的变量有共享和线程私有之分,在parallel for中,凡是for循环中声明的变量都是线程私有的,每个线程都有一份,而for循环外声明的为所有线程共享的。

§例2、向量内积

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 文件:vector_dot_omp.cpp
#include <iostream>
#include <vector>

int main() {
    const int n = 10;
    std::vector<int> v1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::vector<int> v2 = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};

    int sum = 0;

    #pragma omp parallel for reduction(+:sum)
    for (int i = 0; i < n; ++i)
        sum += v1[i] * v2[i];

    std::cout << sum << std::endl;

    return 0;
}

编译:g++ -fopenmp vector_dot_omp.cpp -o vector_dot_omp

执行:

1
2
$ ./vector_dot_omp
220

reduction对一个或多个变量指定一个操作符,每个线程将创建变量的一个私有拷贝,在区域结束处,用指定的操作符将私有拷贝的值进行归约,然后用归约的结果更新原来的变量。如vector_dot_omp.cpp中,每个线程中都有自己的局部变量sum,然后计算一部分和,最后将所有的部分和相加,更新共享的变量sum。

reduction支持8种操作符,创建私有变量时使用的初始值依赖于操作符,如下表所示:

操作符 + - * & | ^ && ||
初始值 0 0 1 ~0 0 0 1 0

显然,分段后的计算结果要与串行结果相同,使用的运算必须满足交换率和结合率,减法是不满足这两条性质的,而上表却出现了减法,这是因为此处的减法和加法的效果基本相同,使用场合为:

1
2
3
#pragma omp parallel for reduction(-:sum)
for (int i = 0; i < n; ++i)
    sum -= v1[i] * v2[i];

它和下面的代码计算结果实际上是相同的:

1
2
3
#pragma omp parallel for reduction(+:sum)
for (int i = 0; i < n; ++i)
    sum -= v1[i] * v2[i];

§OpenMP指令

在c/c++中,OpenMP的基本使用方法为:

1
#pragma omp directives [clause[[,] clause] ... ]

OpenMP的指令有:parallel, for, sections, critical等。

§parallel指令

parallel指令用于定义一个并行执行的代码块。如:

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

int main() {
    #pragma omp parallel
    {
        int i = omp_get_thread_num();
        
        // 连续cout不是线程安全的,只能使用printf
        // std::cout << "Hello from thread " << i << "\n";
        printf("Hello from thread %d\n", i);
    }
}

执行结果:

1
2
3
4
5
6
7
8
Hello from thread 6
Hello from thread 3
Hello from thread 4
Hello from thread 1
Hello from thread 0
Hello from thread 5
Hello from thread 7
Hello from thread 2

§for指令

for指令的语法为:

1
2
#pragma omp [parallel] for [clauses]
for_statement

如果不加parallel,后面所跟的for循环只在某一个线程内运行,而如果出现在parallel声明的块内则相当于parallel for。如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <cstdio>
#include <vector>

int main() {
    #pragma omp for
    for (int i = 0; i < 4; ++i)
        printf("%d ", i);
    printf("\n");

    #pragma omp parallel
    {
        #pragma omp for
        for (int i = 0; i < 4; ++i)
            printf("%d ", i);
    }
    printf("\n");

    return 0;
}

执行结果为:

1
2
0 1 2 3 
0 3 1 2

§sections指令

sections指令用于指定分别在多个线程内执行的代码块,语法为:

1
2
3
4
5
6
7
#pragma omp [parallel] sections [clauses]
{
    #pragma omp section
    {
        code_block
    }
}

例如:

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

int main() {
    #pragma omp parallel sections
    {
        #pragma omp section
        printf("section1, thread id is %d\n", omp_get_thread_num());

        #pragma omp section
        printf("section2, thread id is %d\n", omp_get_thread_num());

        #pragma omp section
        printf("section3, thread id is %d\n", omp_get_thread_num());

        #pragma omp section
        printf("section4, thread id is %d\n", omp_get_thread_num());
    }

    return 0;
}

执行结果为:

1
2
3
4
section 3, thread id is 5
section 1, thread id is 6
section 2, thread id is 1
section 4, thread id is 7

可见的确是在不同的线程中执行的。

§critical指令

critical指令用于指定一个代码块为临界区。语法为:

1
2
3
4
#pragma omp critical [(name)]
{
    code_block
}

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums = {2, 3, 12, 43, 4, 21, 54, 35, 34, 5, 2435, 3425, 34, 5};

    int max = nums[0];

    #pragma omp parallel for
    for (int i = 1; i < nums.size(); ++i) {
        if (nums[i] > max) {
            #pragma omp critical
            {
                if (nums[i] > max)
                    max = nums[i];
            }
        }
    }

    std::cout << max << std::endl;

    return 0;
}

执行结果为:3425

这个程序设计得不好,求最大值或者最小值显然是满足交换率和结合率的,应当使用归约来求。

§OpenMP子句

在c/c++中,OpenMP的基本使用方法为:

1
#pragma omp directives [clause[[,] clause] ... ]

OpenMP子句有:if, num_threads, private, reduction等。

§if子句

if子句用于指定接下来的代码块是否并行,可以出现在parallel、for和sections后面。如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
std::vector<int> add(const std::vector<int> &v1,
                     const std::vector<int> &v2,
                     bool parallel = true) {
    std::vector<int> v3(v1.size());
    
    #pragma omp parallel for if (parallel)
    for (int i = 0; i < v1.size(); ++i)
        v3[i] = v1[i] + v2[i];
    
    return v3;
}

当传入的参数parallel为false时两个向量执行串行加法。

§num_threads子句

num_threads子句用于指定并行的线程数量,可以出现在parallel、for和sections后面。如sections指令一节的示例程序输出结果的线程编号不是预期的0, 1, 2, 3的排列,这是因为默认使用8个线程进行并行,将其改成4个线程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#pragma omp parallel sections num_threads(4)
{
    #pragma omp section
    printf("section1, thread id is %d\n", omp_get_thread_num());
    
    #pragma omp section
    printf("section2, thread id is %d\n", omp_get_thread_num());
    
    #pragma omp section
    printf("section3, thread id is %d\n", omp_get_thread_num());
    
    #pragma omp section
    printf("section4, thread id is %d\n", omp_get_thread_num());
}

执行结果为:

1
2
3
4
section4, thread id is 3
section3, thread id is 2
section2, thread id is 0
section1, thread id is 1

§private子句

private子句用于将一个或多个变量声明成线程私有的变量,线程间无法相互访问这个变量,也无法访问并行区域外的同名共享变量。例如:

1
2
3
4
5
int n = 1;
#pragma omp parallel for private(n)
for (n = 8; n < 12; ++n)
    printf("%d\n", n);
printf("%d\n", n);

执行结果为:8 11 10 9 1

因此以private声明的变量和并行区域外的同名变量没有任何联系。

§reduction子句

reduction的功能如例2所述,可以出现在parallel、for和sections之后。

§OpenMP函数

在c/c++中,使用OpenMP的函数需要包含头文件<omp.h>。以下为一个简单的函数列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 设置并行区域线程数
void omp_set_num_threads(int _Num_threads);

// 返回当前线程数目,在串行代码中调用将返回1
int omp_get_num_threads(void);

// 返回程序的最大可用线程数量
int omp_get_max_threads(void);

// 返回当前线程id,从0开始编号
int omp_get_thread_num(void);

// 返回程序可用的处理器数
int omp_get_num_procs(void);

// 启用或禁用可用线程数的动态调整(缺省情况下启用动态调整)。
void omp_set_dynamic(int _Dynamic_threads);

// 确定在程序中此处是否启用了动态线程调整。
int omp_get_dynamic(void);

// 确定线程是否在并行区域的动态范围内执行
int omp_in_parallel(void);

// 启用或禁用嵌套并行操作
void omp_set_nested(int _Nested);

// 确定在程序中此处是否启用了嵌套并行操作
int omp_get_nested(void);


// 初始化一个(嵌套)互斥锁
void omp_init_lock(omp_lock_t * _Lock);
void omp_init_nest_lock(omp_nest_lock_t * _Lock);

// 销毁一个(嵌套)互斥锁并释放内存
void omp_destroy_lock(omp_lock_t * _Lock);
void omp_destroy_nest_lock(omp_nest_lock_t * _Lock);

// 获得一个(嵌套)互斥锁
void omp_set_lock(omp_lock_t * _Lock);
void omp_set_nest_lock(omp_nest_lock_t * _Lock);

// 释放一个(嵌套)互斥锁
void omp_unset_lock(omp_lock_t * _Lock);
void omp_unset_nest_lock(omp_nest_lock_t * _Lock);

// 试图获得一个(嵌套)互斥锁,并在成功时放回真,失败时返回假
int omp_test_lock(omp_lock_t * _Lock);
int omp_test_nest_lock(omp_nest_lock_t * _Lock);

// 从过去的某一时刻经历的时间,一般用于成对出现,进行时间比较
double omp_get_wtime(void);

// 得到clock ticks的秒数
double omp_get_wtick(void);

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <vector>
#include <random>
#include <omp.h>

int main() {
    const int N = 100000000;   // 一亿
    std::vector<double> A(N);

    std::uniform_real_distribution<double> dist(0, 10);
    std::random_device rd;

    double start = 0;
    double end = 0;
    double elapsed = 0;

    // 串行
    start = omp_get_wtime();
    std::default_random_engine re{rd()};
    for (int i = 0; i < N; i++)
        A[i] = dist(re);
    end = omp_get_wtime();
    elapsed = end - start;
    std::cout << "generate " << N << " numbers in series elapsed: "
              << elapsed << " s" << std::endl;

    // 并行
    start = omp_get_wtime();        
    #pragma omp parallel
    {
        int num_threads = omp_get_num_threads(); 
        std::default_random_engine re{rd()};
        for (int i = omp_get_thread_num(); i < N; i += num_threads)
            A[i] = dist(re);
    }
    end = omp_get_wtime();
    elapsed = end - start;
    std::cout << "generate " << N << " numbers in parallel elapsed: "
              << elapsed << " s" << std::endl;
    
    return 0;
}

执行结果为:

1
2
generate 100000000 numbers in series elapsed: 6.45648 s
generate 100000000 numbers in parallel elapsed: 1.18125 s

§参考

[1] OpenMP标准:https://www.openmp.org/wp-content/uploads/OpenMP-API-Specification-5.0.pdf.

加载评论