OpenMP

OpenMP

OpenMP是一种支持多平台的共享内存、多处理器(多线程)的规范APIOpenMP的API包括编译器伪指令(pragma指令)、运行时函数、环境变量几个部分。OpenMP简化了并行代码的编写,许多细节都由库本身去管理(但依赖关系、读写冲突、死锁等要靠开发者),同时如果系统不支持OpenMP,代码也会直接采用串行执行方式。 OpenMP适用的情况是基于共享内存模型,如果是非共享内存模型(例如分布式),则更适合适用OpenMPI

1. 编译

使用了OpenMP的程序要在编译时加入-fopenmp参数

g++ -fopenmp filename.cpp

2. 执行模型

OpenMP线程采用的是fork-join方式,在需要产生线程的地方通过fork的方式产生线程并使用共享内存的方式来并行,最后自动使用join回收线程。

比较新的OpenMP支持嵌套线程(nest),在外层循环中执行到parallel指定的位置时,OpenMP会产生多个线程,接着如果在内层循环中再遇到了parallel指定的并行,则会再产生多个线程。但这个功能需要系统支持,不支持的系统会忽略内层的并行。

  1. task:相当于一个函数部分(就像是ray中的task一样,是任务的一部分),可以使用task构造块或者section指令构造块,其必须在声明了parallel指令的循环内
  1. target: 用于异构并行计算内容,执行到target构造块的代码时,系统会在加速器上映射相关的数据结构,用加速器来运行代码。

3. 内存模型

OpenMP中,所有线程都可以访问同一个内存空间,但是由于弱内存一致性,每个线程在同一时刻看到在内存空间中的同一变量可能值是不一致的。

  1. 块内变量访问权限:
    • shared(default):并行区域访问到的是真正的变量。
    • private:访问到的仅仅是副本。
  1. 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. 构造

  1. 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;
}
```
  1. for

    #pragma omp parallel for num_threads(3) shared(x, y)
    

    parallel for构造块里的代码是将任务分割分配到多个进程中,实现并行完成一个任务。

    需要注意的是,OpenMP支持的for构造形式有以下限制

    • 可推测循环次数、索引是整型、自增步长不变

    • 循环是单出口单入口,不允许使用breakgotoreturn等语句跳出循环。

    • 不能在循环内部抛出异常,但可以使用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;
}
```
  1. simd

    #pragma omp simd 子句
    

    SIMD可以使得代码向量化,也有点像SSE、AVX等向量化指令集,需要OpenMP 4.0以上的版本才支持该伪指令。可以单独使用,也可以和parallelparallel 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;
}
```
  1. 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;
}
```
  1. sections/section

    将代码分成几个段落,每个段落使用一个线程执行,互不相干。

    #pragma omp parallel section
    
  1. single

    指定某些代码只使用一个线程运行,是仅对某些代码进行特殊处理时才使用的。

    #pragma omp single
    
  1. 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;
    }
    
  1. proc_bind

    处理器映射策略,有以下几种策略

    • spread:均匀分布在各个核心

    • close:尽量分布在相邻的处理器

    • master:主线程和子线程都在同一处理器

  1. nowait

    显示地取消了栅栏同步。

7. 子句

  1. collapse

    为了负载均衡(例如紧随其后的两个循环,每个循环都较小),合并之后任务较小的循环

    // 意思是指定接下来的两层循环直接线程化
    #pragma omp parallel for collapse(2)
    for(int i = 0; i < 3; i++){
        for(int j = 0; j < num; j++){
            // .....
        }
    }
    
  1. private

    将某个线程私有化,每个线程拥有变量的副本,且不允许其他线程染指这些变量。

  1. shared

    声明某些变量是共享的,但必须保证读写的正确性。

  1. reduction

    在并行区域结束时,对指定的某个变量进行某个计算操作(例如+,支持的运算符只有少数)。

  1. schedule

    schedule(type[, size])
    

    其中,size是可选参数,表示每次分发任务的循环迭代次数。而type为策略,主要有以下几种

    • static:静态负载均衡策略,设任务长度为K, 而线程数量为N,则每个线程分得的任务为 K/N。因为 K/N不一定是整数,所以存在一定的负载均衡问题。
    • dynamic:动态负载均衡策略,根据设定的size(或默认)将任务分割,加入工作队列,哪个线程空闲就去领取新的任务,直到所有任务被执行完毕。
    • runtime:由环境变量 OMP_SCHEDULE的值来确定负载均衡策略。
  1. if

    根据所给条件,来决定该区域什么时候并行,什么时候不采取并行。

你可能感兴趣的:(OpenMP)