- cpu核数扩展性问题:创建的线程数量需要随cpu核数变化,不能创建固定数量的线程
- 方便性问题:负载要均衡
- 可移植问题:不存在可移植问题,openmp为标准规范
OpenMP是由一组计算机硬件和软件供应商联合定义的应用程序接口(API);
OpenMP为基于共享内存的并行程序的开发人员提供了一种便携式和可扩展的编程模型,其API支持各种架构上的C/C++和Fortran;
openmp=编译器指令+库函数+环境变量;
OpenMP编程模型
共享内存模型
分叉-合并模型
- OpenMP采用分叉-合并模型(fork-join)实现并行化
- 所有的OpenMP程序都从一个 主线程 开始。主线程串行执行,直到遇到第一个并行区域
- 分叉:之后主线程将创建一组并行线程。
- 并行区域内的代码被用大括号包围起来,然后在多个并行线程上被并行执行。
- 合并:当并行线程执行完成并行区域内的代码之后,它们进行同步并且自动结束,只剩下主线程。
输入输出 (I/O)
- OpenMP没有对并行I/O做出规定,所以对于多个线程尝试读写同一个文件的情况要特别小心;
- 但如果每个线程对不同的文件进行I/O操作,则问题并不重要;
- 编程者有完全责任确保I/O在多线程中被正确地执行
动态线程
- API也提供了运行时环境,来动态第更改用于执行并行区域的线程数,在有可能的情况下尽可能地有效利用已有资源
- 软硬件实现中可能支持,也可能不支持此功能
OpenMP API总览
- 编译器指令(44个)
- 运行时库函数(35个)
- 环境变量(13条个)
编译器指令
编译器指令在你的源代码中可能被显示为注释,并且被编译器所忽略,除非你明显地告诉编译器
OpenMP的编译器指令的目标主要有:
- 产生一个并行区域
- 划分线程中的代码块
- 在线程之间分配循环迭代
- 序列化代码段
- 同步线程间的工作
编译器指令 | 作用 |
---|---|
parallel | 用在一个结构块之前,表示这段代码将被多个线程并行执行 |
for | 用于for循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担,必须由编程人员自己保证每次循环之间无数据相关性 |
parallel for | parallel和for指令的结合,也是用在for循环语句之前,表示for循环体的代码将被多个线程并行执行,它同时具有并行域的产生和任务分担两个功能 |
sections | 用在可被并行执行的代码段之前,用于实现多个结构块语句的任务分担,可并行执行的代码段各自用section指令标出(注意区分sections和section) |
parallel sections | parallel和sections两个语句的结合,类似于parallel for |
single | 用在并行域内,表示一段只被单个线程执行的代码 |
critical | 用在一段代码临界区之前,保证每次只有一个OpenMP线程进入 |
flush | 保证各个OpenMP线程的数据影像的一致性 |
barrier | 用于并行域内代码的线程同步,线程执行到barrier时要停下等待,直到所有线程都执行到barrier时才继续往下执行 |
atomic | 用于指定一个数据操作需要原子性地完成 |
master | 用于指定一段代码由主线程执行 |
threadprivate | 用于指定一个或多个变量是线程专用,后面会解释线程专有和私有的区别 |
例如: #pragma omp parallel default(shared) private(beta,pi)
运行时库函数
Openmp库函数是不断增长的,目的是:
- 设置和查询线程数
- 查询线程的唯一标识符(ID),线程的祖先标识符,或者线程组的大小等
- 设置和查询动态线程的属性
- 查询是否在并行区域,以及在什么级别的并行区域中
- 设置和查询嵌套并行
- 设置、初始化以及终止锁或者嵌套锁
- 查询挂钟时间和分辨率
库函数 | 目标 |
---|---|
OMP_SET_NUM_THREADS | 设置在下一个并行区域中使用的线程数 |
OMP_GET_NUM_THREADS | 返回当前处于执行调用的并行区域中的线程数 |
OMP_GET_MAX_THREADS | 返回调用OMP_GET_NUM_THREADS函数可以返回的最大值 |
OMP_GET_THREAD_NUM | 返回组内线程的线程号(译注:不要和线程总数搞混) |
OMP_GET_THREAD_LIMIT | 返回可用于程序的最大OpenMP线程数 |
OMP_GET_NUM_PROCS | 返回程序可用的处理器数 |
OMP_IN_PARALLEL | 用于确定正在执行的代码是否是并行的 |
OMP_SET_DYNAMIC | 启动或者禁用可执行并行区域的线程数(由运行时系统)的动态调整 |
OMP_GET_DYNAMIC | 用于确定是否启动了动态线程调整 |
OMP_SET_NESTED | 用于启用或者禁用嵌套并行 |
OMP_GET_NESTED | 用于确定嵌套并行是否被弃用 |
OMP_SET_SCHEDULE | 当“运行时”被用作OpenMP指令中的调度类型时,设置循环调度策略 |
OMP_GET_SCHEDULE | 当“运行时”被用作OpenMP指令中的调度类型时,返回循环调度策略 |
OMP_SET_MAX_ACTIVE_LEVELS | 设置嵌套并行区域的最大数量 |
OMP_GET_MAX_ACTIVE_LEVELS | 返回嵌套并行区域的最大数量 |
OMP_GET_LEVEL | 返回嵌套并行区域的当前级别 |
OMP_GET_ANCESTOR_THREAD_NUM | 给定当前线程的嵌套级别,返回其祖先线程的线程号 |
OMP_GET_TEAM_SIZE | 给定当前线程的嵌套级别,返回其线程组的大小 |
OMP_GET_ACTIVE_LEVEL | 返回包含调用任务的的嵌套活动并行区域的数量 |
OMP_IN_FINAL | 如果在最终任务区域中执行该例程,则返回true;否则返回false |
OMP_INIT_LOCK | 初始化与锁变量相关联的锁 |
OMP_DESTROY_LOCK | 解除给定的锁变量与所有锁的关联 |
OMP_SET_LOCK | 获取锁的所有权 |
OMP_UNSET_LOCK | 释放锁 |
OMP_TEST_LOCK | 尝试设置锁,但是如果锁不可用,则不会阻止 |
OMP_INIT_NEST_LOCK | 初始化与锁定变量关联的嵌套锁 |
OMP_DESTROY_NEST_LOCK | 将给定的嵌套锁变量与所有锁解除关联 |
OMP_SET_NEST_LOCK | 获取嵌套锁的所有权 |
OMP_UNSET_NEST_LOCK | 释放嵌套锁 |
OMP_TEST_NEST_LOCK | 尝试设置嵌套锁,但如果锁不可用,则不会阻止 |
OMP_GET_WTIME | 提供便携式挂钟计时程序 |
OMP_GET_WTICK | 返回连续时钟之间的秒数(双精度浮点值) |
环境变量
OpenMP提供了一些环境变量,用来在运行时对并行代码的执行进行控制
- 设置线程数
- 指定循环如何划分
- 将线程绑定到处理器
- 启用/禁用嵌套并行,设置最大的嵌套并行级别
- 启用/禁用动态线程
- 设置线程堆栈大小
- 设置线程等待策略
环境变量 | 作用 |
---|---|
OMP_SCHEDULE | 仅仅适用于for, parallel for指令在调度从句被设置为RUNTIME的情况。该变量的值确定了处理器中的循环迭代如何被调度 |
OMP_NUM_THREADS | 设置在运行中可用的最大线程数 |
OMP_DYNAMIC | 启用或者禁用在执行并行区域时可用线程数的动态调整。其合法的值为TRUE或者FALSE |
OMP_PROC_BIND | 启用或者禁用与处理器绑定的线程,有效值为TRUE或者FALSE |
OMP_NESTED | 启用或者禁用嵌套并行,其有效值为TRUE或者FALSE |
OMP_STACKSIZE | 用于控制所创建的线程(非主线程)的栈空间大小 |
OMP_WAIT_POLICY | |
OMP_MAX_ACTIVE_LEVELS | |
OMP_THREAD_LIMIT |
例如: export OMP_NUM_THREADS=8
OpenMP程序编译
编译器 | 命令 | openmp标识 |
---|---|---|
icc | icc/icpc | -qopenmp |
gcc | gcc/g++ | -fopenmp |
常用Openmp指令
parallel
parallel语句后面要跟一个大括号对将要并行执行的代码括起来;
parallel块中每行代码都被多个线程重复执行;
for
将一个for循环分配到多个线程中执行,不过要和parallel结合起来才会有效果;
parallel for
for循环体的代码将被多个线程并行执行,它同时具有并行域的产生和任务分担两个功能;
一个parallel块中可以有多个for:
#pragma omp parallel
{
#pragma omp for //块1
do something;
#pragma omp for
do something; //块2
}
线程会被均匀的分配到不同的for快上去执行;
section和sections
- section语句是用在sections语句里用来将sections语句里的代码划分成几个不同的段,每段由一个线程执行,各线程工作量总和等于原来的工作量;
- 需要注意的是,这种方式需要保证各个section里的代码执行时间相差不多,否则某个section执行时间比其他section过长就达不到并行执行的效果了;
#pragma omp [parallel] sections [子句]
{
#pragma omp section
{
代码块
}
#pragma omp section
{
代码块
}
}
private
- 用于将一个或多个变量声明成线程私有的变量
- 变量声明成私有变量后,指定每个线程都有它自己的变量私有副本,其他线程无法访问私有副本
- 循环迭代变量在循环构造区域里是私有的
- 声明在循环构造区域内的自动变量都是私有的
- shared来修饰循环迭代变量,也不会改变循环迭代变量在循环构造区域中是私有的这一特点
firstprivate:继承并行区域外的变量值
lastprivate:将并行区域内变量的值传递出去
shared: 线程无关变量都是shared
threadprivate:定义全局私有变量;
threadprivate和private的区别在于threadprivate声明的变量通常是全局范围内有效的,而private声明的变量只在它所属的并行构造中有效;
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
int g=100;
#pragma omp threadprivate(g)
int main(int argc, char *argv[]){
int i,j;
int num_core=omp_get_num_procs();
printf("machine core =========== %d\n",num_core);
printf("%d-----\n",g); //g=100
#pragma omp parallel num_threads(4)
{
g=omp_get_thread_num();
printf("threadID: %d\n",g);
}
printf("%d-----\n",g); //g=0
#pragma omp parallel num_threads(6)
{
printf("g=%d,threadID: %d\n",g,omp_get_thread_num());
}
printf("%d-----\n",g); //g=0
return 0;
}
threadprivade总结:
- 作用于全局变量,变量应定义在main函数外面
int g=100;
#pragma omp threadprivate(g)
- 各个线程对threadprivade变量有自己的备份,互不影响,即使线程被销毁,重新开辟时各线程值依旧不变
shared
- shared子句可以用于声明一个或多个变量为共享变量
- 共享变量,是值在一个并行区域的线程组内的所有线程只拥有变量的一个内存地址,所有线程访问同一地址
default
- default指定并行区域内变量的默认属性
- default(shared):表示并行区域内的共享变量在不指定的情况下都是shared属性
- default(none):表示必须显式指定所有共享变量的数据属性,否则会报错,除非变量有明确的属性定义(比如循环并行区域的循环迭代变量只能是私有的)如果一个并行区域,没有使用default子句,那么其默认行为为default(shared)
reduction
- 用来对一个或多个变量进行归约操作
- 每个线程将创建reduction变量的一个私有拷贝(变成私有的)
- 在并行区域的结束处,各个线程将私有拷贝的值进行归约