OpenMP入门

  • 一种简单的方式, 让程序员不需要懂得创建和销毁线程就能写出多线程化程序
  • 只需要告诉OpenMP哪里需要被线程化, 剩下的由OpenMP决定
  • 头文件 #include <omp.h>
  • 编译
    • g++ filename.cpp -fopenmp -o filename
    • gcc filename.c -fopenmp -o filename
    • 好像-fopenmp--openmp等价???
  • 一般结构
    • 标记并行块 #pragma omp parallel
    • 标记单个并行单元 #pragma omp single/for/section(s)/task
    • 可以简写为 #pragma omp parallel for 等等
    • 外部设备 #pragma omp target
#include <omp.h>
int main() {
#pragma omp parallel
{
#pragma omp for
}
}
  • #pragma omp <directive> (clause[ [,] clause]...) {block} 基本结构
    • #pragma omp 编译制导语句, 代码块周围要有 {大括号}
    • #pragma omp parallel {}基本的循环
    • #pragma omp parallel for 并行部分包含一个for循环
    • #pragma omp critical 并行部分的代码 每次只能由一个线程执行(等效于取消并行)
    • #pragma omp barrier 同步并行线程, 所有线程等待, 直到所有线程都执行到本行
    • #pragma omp section 将并行块内部的代码划分给线程组中的各个线程, 一般会在内部嵌套几个独立的section语句,可以使用nowait来停止等待
      • 没看懂
  • omp_set/get_XXX 函数
    • void omp_set_num_threads(int) 手动设置线程数(可以自动设置)
      • 或者通过#pragma omp parallel num_threads(int)进行设置
    • int omp_get_thread_num(void) 获取当前线程的编号
    • int omp_get_num_threads(void) 获取线程总数
  • 基础情况
#include <stdio.h>
#include <omp.h>
int main(int argc, char** argv) {
    int num_thread = 4;
    omp_set_num_threads(num_thread);    // 设定线程数
#pragma omp parallel num_threads(7)     // 然后改成7线程了
{
    int id = omp_get_thread_num();  // 查询本线程号
    printf("hello from thread%d\n",id);
}
    return 0;
}
  • #pragma omp parallel for
  • 使用“working share”, 使每一个循环均可分配到不同的不同的线程, 并且保证只执行一次.
  • 苛刻要求
    • 循环变量必须是整形int (unsigned就不行)
    • 循环比较条件必须是< <= > >=
      • 如果< <=, 则循环变量必须增加; 反之减小
      • 但是自己试了试好像!=也行???(但是教程的意思是不行)
    • 循环的第三个表达式, 必须是整数的加减, 且加减的值必须是循环不变量
      • 可以使用++ --
    • 循环必须是单入口、单出
      • 也就是说循环内部不允许能够达到循环以外的跳转语句,exit除外
      • 异常的处理, 也必须在循环体内处理
      • 例如:若循环体内的breakgoto会跳转到循环体外,那么会编译不通过。
    • 不通过例子 for (unsigned int i = 0; i != 10; i = i+1) {}
      • 但是好像通过了???
  • 竞态条件: 每次迭代都依赖于另一个不同的迭代
    • 实际很难被编译器检测到, 只能重写或者选择不同算法
    • 通过合适修改消除竞态条件(第二段代码)
      • 尽量依赖循环变量进行更改, 保持引入的非内部参数不变
#pragma omp parallel for
    for (char i = 'a'; i <= 'z'; i++)   // i是循环变量
        std::cout << i << std::endl;    // 可以执行, 但是endl会冲
        fact[i] = i * fact[i-1];    // 不会报错, 但是会出错. 竞态条件
    return 0;
#pragma omp parallel for
    for (int i = 0; i < numElements; i++) {
        array[i] = initValue + i;   // pass
        // array[i] = initValue++;  // error
    }
  • 分类
    • 共有shared: 所有线程访问的都是相同的内存地址
    • 私有private: 每个线程都有自己的一份拷贝
    • 默认情况下, 只有循环变量是私有, 其他变量默认共有
  • 设定私有
    • 在循环内部声明变量, 注意private不能是static! 会冲突.
    • 通过OpenMP指令声明私有变量
    int temp, array[10];
#pragma omp parallel for private(temp) shared(array)
// 其实share(array)不写也没关系
    for (int i = 0; i < 10; i++) {
        temp = array[i];
        array[i] = doSomething(temp);
    }
  • 针对累计运算变量的循环
  • 为每个线程提供了私有的可累加变量
  • 线程退出时, 把每个线程的部分综合
  • 可以自定义归并操作 |操作|+ -|*|&|||^|&&|||| |私有临时变量初始值|0|1|~0|0|0|1 true|0 false|
    int sum = 0;
#pragma opm parallel for reduction(+:sum)
    for (int i = 0; i < 100; i++) {
        sum += array[i];
    }
  • 用于for循环
  • 希望实现负载均衡, 不出现空闲时间. 需要进行分配上的优化
    • 需要提供一些信息给OpenMP, 告知有可能的优化方式
    • 默认循环时间相同, 把不同迭代等分到不同核心上, 同时尽可能减小内存访问冲突
      • 内存访问为线性, 从这个角度出发, 建议把循环按顺序等分较好.
      • 但是有可能导致负载均衡降低
    • 所以需要综合考虑内存访问负载均衡
  • #pragma omp parallel for schedule(<kind>, <chunk size>)
    • kind = dynamic / static / guided / runtime
      • static:
        • 每个线程自行决定要执行哪块, 即每个线程执行for循环中的一个子块<chunk>(连续一坨要执行的迭代值)
        • schedule的默认值
        • 指定每次索取的子块大小<chunk size>, 默认1. 即按顺序线程按顺序每次索取<chunk size>
      • dynamic:
        • 每次都向omp库索取一个迭代值, 因此线程有可能执行任意的迭代值, 而不是子块
        • 指定每次索取的子块大小<chunk size>, 默认1
        • 动态分配本身也有资源消耗, 所以需要制定子块大小
      • guided
        • 一种采用指导性的启发式自调度方法
        • 开始时每个线程会分配到较大的迭代块, 之后分配到的迭代块会逐渐递减
          • 迭代块的大小会按指数级下降到指定的<chunk size>大小
          • 如果没有指定<chunk size>参数, 那么迭代块大小最小会降到1
      • runtime (并不是真实调度方式)
        • 在运行时根据环境变量OMP_SCHEDULE来确定调度类型
        • 最终使用的调度类型仍然是上述三种调度方式中的某种
          • 例如在unix系统中, 可以使用setenv命令来设置OMP_SCHEDULE环境变量: setenv OMP_SCHEDULE "dynamic, 2"
          • 没搞懂在哪里用这句…
  • 例子
    • #pragma omp parallel for schedule(static)
      • 等价于不加, #pragma omp parallel for的默认情况
    • #pragma omp parallel for schedule(dynamic, 3)
      • 并行, 动态分配, 每次索取三个迭代值
    • #pragma omp parallel for schedule(guided, 4)
      • 并行, 导引分配, 分配数从5降到4(为什么从5开始下降??)
  • 多层循环, 可以保持循环的层次
  • 似乎只能是多层循环, 且两层循环层间不能有东西?
#pragma omp parallel for collapse(2)
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 6; j++) {
            workintwocycles(i, j);
        }
        // 层间可以有东西吗?
    }
  • 一个线程按代码顺序生成任务, 放入任务池中
  • 空闲的线程(包括生成任务的线程)取出任务执行
  • 优点: 动态的处理任务分配, 而非前述的静态分配
  • #pragma ompp task 用于single中
    • 图例

avatar

avatar

  • 定义于omp.h, 等效于mutex
  • 有5种操作
    • 初始化锁 void omp_init_lock(omp_lock_t*)
      • 初始化后锁处于未锁定状态
    • 销毁锁 void omp_destroy_lock(omp_lock_t*)
      • 调用这个函数时, 锁必须是未锁定状态
    • 获得锁 void omp_set_lock(omp_lock_t*)
      • 如果锁已经被其他线程加锁了, 那当前线程进入阻塞状态
    • 释放锁 void omp_unset_lock(omp_lock_t*)
      • 调用这个方法的线程必须已经获得了锁, 如果当前线程没有获得锁, 则会有未定义行为
    • 尝试获取锁 int omp_test_lock(omp_lock_t*)
      • 获取锁成功则返回1, 否则返回0
      • 该函数可以看作是omp_set_lock的非阻塞版本
  • 例子
    • 只能有一个线程执行锁范围中的内容
#include <iostream>
#include <omp.h>
static omp_lock_t lock;
int main() {
    omp_init_lock(&lock); //初始化互斥锁
#pragma omp parallel for
    for(int i = 0; i < 5; ++i) {
        omp_set_lock(&lock);   //获得互斥器
        std::cout << omp_get_thread_num() << "+" << std::endl;
        std::cout << omp_get_thread_num() << "-" << std::endl;
        omp_unset_lock(&lock); //释放互斥器
    }
    omp_destroy_lock(&lock);  //销毁互斥器
    return 0;
}
  • 一个section内的block必须串码运行, 不同section之间可以并行
  • 如下, parallel sections {}调出, section {}指定串行内容
#pragma omp parallel sections
{
    {work1();}
#pragma omp section
    {work2();
    work3();}
#pragma omp section
    {work4();}
}
  • 保证其内代码只在一个线程中执行
    • #pragma omp critical {}
  • 可以给一个名字, critical(criname)
    • 相同名字的代码块们, 保证不会在多个线程中运行, 同一时间只会有一个在运行
    • 如果没有命名, 系统会自己给一个名称
#pragma omp critical(dataupdate)
{
datastructure.reorganize();
}
#pragma omp critical(dataupdate)
{
datastructure.reorganize_again();
}
  • 保证其后续的语句执行时原子性的, 即其执行不可拆分(要么执行成功, 要么什么都没有执行)
  • 只能用于简单指令, 如+= -= *= &=等等
    • counter = counter + value
  • 表达式中也不能够有函数调用, 数组索引等操作
    • (好菜啊)还是用critical吧
    • 但是估计是比较轻量级, 单单执行比critical节省时间
  • 用于控制一段代码在for循环中的执行顺序,它保证这段代码一定是按照for中的顺序依次执行的
  • 强制等待全部线程都完成后再操作
  • #pragma omp barrier
  • 只能由master线程在操作
  • 共有变量: 在parallel之前的变量都在线程中被引用与操作, 为所有线程所共有. 但是由于线程可能在不同的处理器上处理, 所以可能存在延迟, 可强制更新共有变量(伪内存共享)
  • 将循环转化为SIMD循环(矢量化)
  • 多线程执行展示
  • 老师上课提到的<directive>
  • parallel for sections section critical atomic collapse
  • parallel for parallel sections
  • single master barrier flush threadprivate task simd target distribute teams cancel
  • 尽量减少一下使用
    • critical atomic master barrier flush task threadprivate
  • 现在还没有尝试用visual studio — 娄泽坤 2020/12/18 13:54
  • 似乎有一些要注意的地方:
    • 随机数播种, 每个线程应当重新进行
    • rand()函数调用会busy wait
    • 不应使用clock()获取时间, 应该使用double omp_get_wtime()
  • 娄泽坤 2020/12/21 16:18
  • home/students/nqhq/openmp.txt
  • 最后更改: 2020/12/21 16:20
  • 由 鸟雀呼晴