OpenMP学习小结

PhoenixGS
Jul 10, 2022
Last edited: 2022-7-19
type
Post
status
Published
date
Jul 10, 2022
slug
openmp
summary
对使用OpenMP时需要用到的各种函数等进行整理
tags
CS
高性能计算
category
icon
password
Property
Jul 19, 2022 08:35 AM
OpenMP,即Open Multi-Processing的缩写。
显式指导多线程、共享内存并行的API

并行区结构体

#pragma omp parallel [子句]
子句:
private(列表) firstprivate(列表) shared(列表) copyin(列表) reduction([归约修饰符,] 归约操作符: 列表) proc_bind(master | close | spread) allocate([分配器: 列表) if ([parallel: ] 标量表达式) num_thread(整数表达式) default(shared | none)
  • 并行区结束时,所有线程会同步(有一个隐式barrier)

线程数量

调整并行区内线程数量的方式
添加num_threads子句
调用omp_set_num_threads()函数
设置OMP_NUM_THREADS环境变量
添加if子句
优先级:pragma里面指定 > 函数API设置 > 环境变量设置

嵌套并行区

  • 除非启用嵌套并行区,内层并行区只用一个线程执行
启用/禁用嵌套并行区
omp_set_nested(bool)
设置OMP_NESTED环境变量
检查嵌套并行区是否开启
omp_get_nested()

任务分配结构体

  • 任务分配结构体不创建新进程
  • 进入任务分配结构体时,各线程不同步
  • 离开任务分配结构体时,各线程同步
三种典型的任务分配结构体
任务分配结构体需要包含在并行区内部
  1. DO/for
  1. sections
  1. single

任务分配方式:for

#pragma omp for [子句] for (...)
子句:
private(列表) firstprivate(列表) lastprivate([lastprivate修饰符: ]列表) linear(列表[: 步长]) schedule([调度修饰符 [,调度修饰符]:] 调度策略 [,块大小]) collapse(n) ordered[(n)] allocate([分配器: 列表) order(concurrent) reduction([归约修饰符,] 归约操作符: 列表) nowait
以下两种写法等价:
#pragma omp parallel { #pragma omp for for (i = 0; i < 25; i++) { printf("Foo"); } } #pragma omp parallel for for (i = 0; i < 25; i++) { printf("Foo"); }
调度方式
  1. static 将各循环迭代组成块,块数与线程数相等,各块包含的迭代次数尽量平均 每个线程执行一块
  1. dynamic 将各循环迭代分割为块,每块默认只含一次迭代 每当某线程执行完一块,会动态地再请求一块
  1. guided dynamic类似,但块的大小会从大到小动态变化,以改善负载均衡
  1. runtime 运行时再通过环境变量OMP_SCHEDULE设定
  1. auto 由编译器或系统自动决定
  • collapse子句:将嵌套循环的迭代统一调度,而不是串行执行内层循环

任务分配方式:sections

  • 将不同片段的代码组成不同section,再分配给不同线程
  • 各section命令需要包含在一个sections命令中
  • 每个section只被一个线程执行一次
  • 线程和section的对应关系是不确定的
#pragma omp sections [子句] { #pragma omp section 语句块 #pragma omp section 语句块 ... }
子句:
private(列表) firstprivate(列表) lastprivate([lastprivate修饰符: ]列表) allocate([分配器: 列表) reduction([归约修饰符,] 归约操作符: 列表) nowait
#pragma omp parallels num_threads(2) { #pragma omp sections { #pragma omp section { for (int i = 0; i < n; i++) { c[i] = a[i] + b[i]; } } #pragma omp section { for (int i = 0; i < n; i++) { d[i] = a[i] + b[i]; } } } }

任务分配方式:single

  • 可用于执行非线程安全的操作(例如I/O)
  • 不执行此代码段的线程将等待此代码段执行结束(除非用了nowait子句)

数据共享

  • 默认线程间共享变量 全局变量、static变量 除了并行循环的循环下标外,所有并行区内、任务分配结构体外局部变量
  • 默认线程内私有变量 并行循环的循环下标 任务分配结构体内的局部变量 并行区内调用的函数中的局部变量
  • 作用域可以通过子句显式控制 private, shared, firstprivate, lastprivate default, reduction, copyin
  1. private(var_list) 将列表中的变量声明为线程私有 变量在进入并行区时不会初始化,离开并行区后不会保留
  1. shared(var_list) 将列表中的变量声明为线程间共享
  1. firstprivate(var_list)private类似,但变量在进入并行区时会被初始化成进入并行区前的值
  1. lastprivate(var_list)private类似,但变量在离开并行区后,其在最后一次迭代时的值会被保留

线程同步

  1. #pragma omp critical 只有一个线程执行临界区
  1. #pragma omp barrier 等待所有线程到达此处后继续运行
  1. #pragma omp single 只用一个线程执行代码
  1. #pragma omp master 只用主线程执行一段代码(执行完毕后没有隐式barrier)
  1. #pragma omp atomic 使用原子指令访问共享内存
  • OpenMP的锁函数:
omp_set_lock(l); /* 临界区代码 */ omp_unset_lock(l);
void omp_init_lock(omp_lock_t *lock) //初始化锁 void omp_destroy_lock(omp_lock_t *lock) //销毁锁 void omp_set_lock(omp_lock_t *lock) //获取锁。当锁已被其他线程获取时,则等待其他线程释放锁 void omp_unset_lock(omp_lock_t *lock) //释放锁 int omp_test_lock(omp_lock_t *lock) //测试某个锁是否已被获取。此函数不会阻塞
  • 当需要获取一个共享变量的最新值时,必须保证变量被flush
#pragma omp flush [内存序] [(变量列表)] //手动flush acq_rel, release, acquire //内存序 parallel(进出)/critical(进出)/ordered(进出)/for(出)/sections(出)/single(出) //自动flush
nowait子句:取消barrier
#pragma omp for nowait for (int i = 0; i < n; i++) { a[i] = bigFunc1(i); } #pragma omp for nowait for (int j = 0; j < m; j++) { b[j] = bigFunc2(j); }
  • OpenMP同步方法
  1. Barrier
  1. 临界区(底层采用锁实现)
  1. 显式的锁操作
  1. 原子操作Atomic
  1. 单线程任务分配结构体

归约

使用reduction子句
reduction(op: list) //list中的变量需为共享变量

OpenMP示例代码

#include <stdio.h> #include <stdlib.h> #include <omp.h> void Hello(void); /** 线程函数 */ int main(int argc, char* argv[]) { /** 从命令行得到线程数 */ int thread_count = strtol(argv[1], NULL, 10); #pragma omp parallel num_threads(thread_count) //并行指导语句 Hello(); return 0; } /** main */ void Hello(void) { int my_rank = omp_get_thread_num(); //函数调用-线程ID int thread_count = omp_get_num_threads(); //线程数 printf("Hello from thread %d of %d\n", my_rank, thread_count); } /** Hello */

编译运行

gcc −g −Wall −fopenmp −o main main.c
因编译器不同而不同 OMP_NUM_THREADS=4 ./main
环境变量设置

常用的库函数

void omp_set_num_threads(int num_threads) int omp_get_num_threads() int omp_get_thread_num() int omp_get_thread_limit() int omp_get_num_procs() int omp_in_parallel()

常用的环境变量

OMP_NUM_THREADS OMP_PROC_BIND OMP_WAIT_POLICY
notion image

参考资料

翟季冬老师高性能计算及应用课件
MPI学习小结自然语言处理