2555 字
13 分钟
OpenMP学习目录

OpenMP学习目录#

  1. OpenMP
    1. 例1、向量加法
    2. 例2、向量内积
    3. OpenMP指令
      1. parallel指令
      2. for指令
      3. sections指令
      4. critical指令
    4. OpenMP子句
      1. if子句
      2. num_threads子句
      3. private子句
      4. reduction子句
    5. OpenMP函数
    6. 参考

§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、向量加法#

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

执行:

Terminal window
$ ./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、向量内积#

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

执行:

Terminal window
$ ./vector_dot_omp
220

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

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

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

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

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

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

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

§OpenMP指令#

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

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

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

§parallel指令#

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

#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);
}
}

执行结果:

Terminal window
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指令的语法为:

#pragma omp [parallel] for [clauses]
for_statement

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

#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指令#

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

#pragma omp [parallel] sections [clauses]
{
#pragma omp section
{
code_block
}
}

例如:

#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;
}

执行结果为:

Terminal window
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指令用于指定一个代码块为临界区。语法为:

#pragma omp critical [(name)]
{
code_block
}

例如:

#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的基本使用方法为:

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

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

§if子句#

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

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个线程:

#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());
}

执行结果为:

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

§private子句#

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

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>。以下为一个简单的函数列表:

// 设置并行区域线程数
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);

例:

#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;
}

执行结果为:

Terminal window
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.

OpenMP

OpenMP学习目录
https://tuoyuangui.github.io/website/posts/typora/指南/openmp使用指南/
作者
椭圆规/tuoyuangui
发布于
2023-05-12
许可协议
CC BY-NC-SA 4.0