OpenMP
OpenMP
是一种支持多平台的共享内存、多处理器(多线程)的规范与API。OpenMP
的API包括编译器伪指令(pragma
指令)、运行时函数、环境变量几个部分。OpenMP
简化了并行代码的编写,许多细节都由库本身去管理(但依赖关系、读写冲突、死锁等要靠开发者),同时如果系统不支持OpenMP
,代码也会直接采用串行执行方式。OpenMP
适用的情况是基于共享内存模型,如果是非共享内存模型(例如分布式),则更适合适用OpenMPI
。
1. 编译
使用了OpenMP
的程序要在编译时加入-fopenmp
参数
g++ -fopenmp filename.cpp
2. 执行模型
OpenMP
线程采用的是fork-join
方式,在需要产生线程的地方通过fork
的方式产生线程并使用共享内存的方式来并行,最后自动使用join
回收线程。
比较新的OpenMP
支持嵌套线程(nest),在外层循环中执行到parallel
指定的位置时,OpenMP
会产生多个线程,接着如果在内层循环中再遇到了parallel
指定的并行,则会再产生多个线程。但这个功能需要系统支持,不支持的系统会忽略内层的并行。
- task:相当于一个函数部分(就像是
ray
中的task一样,是任务的一部分),可以使用task
构造块或者section指令
构造块,其必须在声明了parallel
指令的循环内。
- target: 用于异构并行计算内容,执行到
target
构造块的代码时,系统会在加速器上映射相关的数据结构,用加速器来运行代码。
3. 内存模型
在OpenMP
中,所有线程都可以访问同一个内存空间,但是由于弱内存一致性,每个线程在同一时刻看到在内存空间中的同一变量可能值是不一致的。
- 块内变量访问权限:
- shared(default):并行区域访问到的是真正的变量。
- private:访问到的仅仅是副本。
- flush操作:会刷新内存空间内变量的值,但是并不保证顺序,所以还是可能会出现不一致。
4. 环境变量
通过修改环境变量的值,可以对OpenMP
进行定制。
环境变量名 | 描述 |
---|---|
OMP_SCHEDULE | 运行时的负载均衡类型与循环次数 |
OMP_NUM_THREADS | 并行区域的默认线程数 |
OMP_DYNAMIC | 是否可以动态调整并行区域的线程数(ture or false) |
OMP_PROC_BIND | 是否允许迁移线程(切换处理器),如果为true,则不会迁移 |
OMP_NESTED | 是否支持嵌套并行区域 |
OMP_STACKSIZE | 指定每个线程的栈的大小 |
OMP_THREAD_LIMIT | 指定了线程最多被创建的数量 |
5. 锁函数
在OpenMP
中,锁的数据类型为omp_lock_t
,其基本功能就是保证线程同步。以下为常见的几个函数:
函数名 | 描述 |
---|---|
void omp_init_lock(omp_lock_t *); | 初始化线程锁 |
void omp_set_lock(omp_lock_t *); | 获得线程锁 |
void omp_nuset_lock(omp_lock_t *); | 释放线程锁 |
void omp_destroy_lock(omp_lock_t *); | 销毁线程锁 |
注:初始化和销毁锁的操作必须由主线程在并行区域外执行。
#include
#include
#include
int main(int argc, char *argv[])
{
int x = 3, y = 4;
// 初始化锁
omp_lock_t lock;
omp_init_lock(&lock);
// 伪指令设置并行,使用num_threads()设置线程数,
// 并且将x、y设置成shared状态
// 花括号必须在下一行,而不能在#pragma后面
#pragma omp parallel num_threads(3) shared(x, y)
{
omp_set_lock(&lock); // 线程获得锁,保证同步
x += omp_get_thread_num(); // 当前的线程id
y += omp_get_thread_num();
omp_unset_lock(&lock); // 释放锁
}
printf("x = %d, y = %d\n", x, y);
omp_destroy_lock(&lock);
return 0;
}
6. 构造
-
parallel
#pragma omp parallel num_threads(3) shared(x, y)
parallel构造块里的代码是将同一份代码复制到多个线程共同运行,也就是说同一个任务被多个线程执行了。
parallel构造块有如下限制
花括号必须在下一行,而不能在#pragma后面
不能使用
nowait()
```c++
#include
#include
using namespace std;
void test_omp_parallel() {
#pragma omp parallel num_threads(3)
for (int i = 0; i < 3; i++)
{
cout << "Hello, I am " << omp_get_thread_num() << ", iter " << i << endl; // 当前的线程id
}
}
int main(int argc, char* argv[])
{
test_omp_parallel();
return 0;
}
```
-
for
#pragma omp parallel for num_threads(3) shared(x, y)
parallel for构造块里的代码是将任务分割分配到多个进程中,实现并行完成一个任务。
需要注意的是,OpenMP支持的for构造形式有以下限制
可推测循环次数、索引是整型、自增步长不变
循环是单出口单入口,不允许使用
break
、goto
、return
等语句跳出循环。不能在循环内部抛出异常,但可以使用
exit()
退出整个程序,其余的线程也会同步退出。
```c++
#include
#include
using namespace std;
void test_omp_for() {
#pragma omp parallel for num_threads(3)
for (int i = 0; i < 3; i++)
{
cout << "Hello, I am " << omp_get_thread_num() << ", iter " << i << endl; // 当前的线程id
}
}
int main(int argc, char* argv[])
{
test_omp_for();
return 0;
}
```
-
simd
#pragma omp simd 子句
SIMD可以使得代码向量化,也有点像SSE、AVX等向量化指令集,需要
OpenMP 4.0
以上的版本才支持该伪指令。可以单独使用,也可以和parallel
、parallel for
联合使用。
simd有以下子句:
* **safelen(x)**:其后紧随的循环中,每x次循环都是互不相关的(也就是每个线程内的数据是不相关的)。
* **aligned(list:n)**:对数组list对齐尺寸为n个字节。
* **reduction(+:ret)**:每个循环的数组都是部分数据,最后将其整合成完整的数据。
```c++
#include
#include
using namespace std;
void test_omp_simd() {
int a[4] = { 1, 2, 3, 4 };
int ret=0;
#pragma omp parallel for simd reduction(+:ret)
{
for (int i = 0; i < 4; i++) {
ret += a[i] * a[i];
}
}
cout << "ret: " << ret << endl;
}
int main(int argc, char* argv[])
{
test_omp_simd();
return 0;
}
```
-
task
#pragma omp task 子句
task主要就是为了方便分解任务,但需要注意的是,
task
构造必须在parallel
构造块之中运行,且必须在parallel构造块中使用single
子句。如果需要父线程等待所有子线程完成才继续执行,则需要使用taskwait
子句。
递归计算Fibonacci数列例子
```c++
#include
#include
using namespace std;
int facobi(int num) {
// 递归退出条件
if ((0 == num) || (1 == num)) {
return 1;
}
int f1, f2;
#pragma omp task shared(f1)
f1 = facobi(num - 1);
#pragma omp task shared(f2)
f2 = facobi(num - 2);
#pragma omp taskwait
return f1 + f2; // 使用taskwait语句,等待f1和f2计算结束再返回
}
void test_omp_task() {
int r;
#pragma omp parallel shared(r)
{
#pragma omp single
r = facobi(5); // 必须使用single子句,指定该任务只有1个线程运行
}
cout << "r: " << r << endl;
}
int main(int argc, char* argv[])
{
test_omp_task();
return 0;
}
```
-
sections/section
将代码分成几个段落,每个段落使用一个线程执行,互不相干。
#pragma omp parallel section
-
single
指定某些代码只使用一个线程运行,是仅对某些代码进行特殊处理时才使用的。
#pragma omp single
-
barrier
栅栏同步是指要执行当前线程,必须保证这一部分之前的所有线程都执行完毕(比如每个并行任务中有几块分开的并行区域,每一个区域执行前,必须保证上一个区域的所有线程都已执行完)。而
barrier
则是显式调用栅栏同步。#include
#include using namespace std; void test_omp_barrier() { int a[6]; #pragma omp parallel num_threads(3) shared(a) { int id = omp_get_thread_num(); // 当前的线程id a[id] = id; a[3 + id] = 3 + id; #pragma omp barrier int swap = a[id]; a[id] = a[5 - id]; a[5 - id] = swap; } for (int i = 0; i < 6; i++) cout << "a[" << i << "]: " << a[i] << endl; } int main(int argc, char* argv[]) { test_omp_barrier(); return 0; }
-
proc_bind
处理器映射策略,有以下几种策略
spread:均匀分布在各个核心
close:尽量分布在相邻的处理器
master:主线程和子线程都在同一处理器
-
nowait
显示地取消了栅栏同步。
7. 子句
-
collapse
为了负载均衡(例如紧随其后的两个循环,每个循环都较小),合并之后任务较小的循环。
// 意思是指定接下来的两层循环直接线程化 #pragma omp parallel for collapse(2) for(int i = 0; i < 3; i++){ for(int j = 0; j < num; j++){ // ..... } }
-
private
将某个线程私有化,每个线程拥有变量的副本,且不允许其他线程染指这些变量。
-
shared
声明某些变量是共享的,但必须保证读写的正确性。
-
reduction
在并行区域结束时,对指定的某个变量进行某个计算操作(例如+,支持的运算符只有少数)。
-
schedule
schedule(type[, size])
其中,size是可选参数,表示每次分发任务的循环迭代次数。而type为策略,主要有以下几种
- static:静态负载均衡策略,设任务长度为K, 而线程数量为N,则每个线程分得的任务为 K/N。因为 K/N不一定是整数,所以存在一定的负载均衡问题。
- dynamic:动态负载均衡策略,根据设定的size(或默认)将任务分割,加入工作队列,哪个线程空闲就去领取新的任务,直到所有任务被执行完毕。
- runtime:由环境变量
OMP_SCHEDULE
的值来确定负载均衡策略。
-
if
根据所给条件,来决定该区域什么时候并行,什么时候不采取并行。