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、向量加法
#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
执行:
$ ./vector_add_omp11 11 11 11 11 11 11 11 11 11OpenMP采用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、向量内积
#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
执行:
$ ./vector_dot_omp220reduction对一个或多个变量指定一个操作符,每个线程将创建变量的一个私有拷贝,在区域结束处,用指定的操作符将私有拷贝的值进行归约,然后用归约的结果更新原来的变量。如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); }}执行结果:
Hello from thread 6Hello from thread 3Hello from thread 4Hello from thread 1Hello from thread 0Hello from thread 5Hello from thread 7Hello 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;}执行结果为:
section 3, thread id is 5section 1, thread id is 6section 2, thread id is 1section 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());}执行结果为:
section4, thread id is 3section3, thread id is 2section2, thread id is 0section1, 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);
// 返回当前线程数目,在串行代码中调用将返回1int 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;}执行结果为:
generate 100000000 numbers in series elapsed: 6.45648 sgenerate 100000000 numbers in parallel elapsed: 1.18125 s§参考
[1] OpenMP标准:https://www.openmp.org/wp-content/uploads/OpenMP-API-Specification-5.0.pdf.