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
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循环外声明的为所有线程共享的。
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];
|
在c/c++中,OpenMP的基本使用方法为:
1
|
#pragma omp directives [clause[[,] clause] ... ]
|
OpenMP的指令有:parallel, for, sections, critical
等。
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指令的语法为:
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;
}
|
执行结果为:
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指令用于指定一个代码块为临界区。语法为:
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
这个程序设计得不好,求最大值或者最小值显然是满足交换率和结合率的,应当使用归约来求。
在c/c++中,OpenMP的基本使用方法为:
1
|
#pragma omp directives [clause[[,] clause] ... ]
|
OpenMP子句有:if, num_threads, private, reduction
等。
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子句用于指定并行的线程数量,可以出现在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子句用于将一个或多个变量声明成线程私有的变量,线程间无法相互访问这个变量,也无法访问并行区域外的同名共享变量。例如:
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的功能如例2所述,可以出现在parallel、for和sections之后。
在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.