浅谈Opem_MP

OpenMP2.5

有底层API后,就已经可以实现并行编程;然而,很多时候串行算法已经成型,如果继续使用原有的底层API,还将面临转换和调试的问题。OpenMP正是为了解决这样的问题。

一、OpenMP的介绍

1.概览

  • 提供线程级别的并行模型
  • 基于共享内存的模型
  • 本身只是提供一种规范
    具体的实现由各个系统和编译器负责实现

2.本质

  • 一套多线程的API
  • 面向程序员的高层接口
  • 提供一系列的编译和预处理的指导语句
  • 主要提供Fortran、C、C++的多线程支持
  • 以SMP的物理结构完成多线程的实现

3.实现层次

  • 编译时的指导语句
  • 库函数的支持
  • 环境变量的支持
  • OpenMP的标准可以实现在任何编译器上
    不同的编译器支持程度不同

4.历史

(略)

5.OpenMP的目标

标准化

  • 在不同的语言和架构上都可以以相同的方式编写多核程序

简洁有效

  • 编译器的指导语句尽可能地少

易用性

  • 允许程序逐步并行化
  • 使对串行程序的修改尽可能地少

可移植性

  • 多种语言
  • 不同平台

6.OpenMP编程模型

共享内存、基于线程的并行模型

显式并行

Fork-Join模型

  • 程序启动后是单线程
  • 达到需要并行的部分(并行区)时,产生多个线程同时运行
  • 所有线程同时执行完后互相等待,一起结束

基于编译器指导语句

支持嵌套并行

动态线程的创建与销毁

线程的数量可以由OpenMP自适应

I/O

  • OpenMP并没有指定I/O的接口,仍然按原有的方式进行读写
  • 因此并行区中的读写会面临冲突的问题,需要程序员自己解决

内存模型

7.OpenMP的层次

  • SMP的硬件结构
  • 系统的线程支持与OpenMP的运行时库
  • 编译器指导语句、库函数和环境变量
  • 应用程序和最终用户

8.示例代码

    #include <omp.h>
    
    void main()
    {
        #pragma omp parallel            //编译指导语句,将大括号括起的范围内做成一个并行区
        {
            int ID=omp_get_thread_num();
            printf("hello(%d)",ID);
            printf("world(%d)\n",ID);
        }
    }

编译时,需要增加参数-fopenmp(gcc)、-mp(pgi)、/Qopenmp(Intel)、/openmp(Visual Studio,或直接在项目属性中添加OpenMP支持)

更一般的形式

    #include <omp.h>
    int main()
    {
        int v1,v2,v3;
        //Serial code
        #pragma omp parallel private(v1,v2) shared(v3)
        {
            //
            //Join
        }
        //Back to serial code
    }
  • 大括号必须紧跟编译指导语句书写
  • 语法格式是固定的

二、创建线程

1.Fork-Join结构

  • 主线程按那些创建一组线程执行并行任务
  • 并行区完全可以嵌套
    • 并行区中,主线程担任一个线程的工作
    • 子并行区中,仍有相应概念上的主线程

2.指定线程的个数

虽然线程个数可以由OpenMP自动指定,但是也可以手动设置

omp_set_num_threads(4);

这使得此函数之后的每个并行区都是4个线程同时运行

也可以使用指导语句,这样只对一个并行区生效

`#pragma omp parallel num_threads(4)`

三、同步方式

1.临界区

多线程同时只能由一个进入临界区执行

    float res;
    #pragma omp parallel
    {
        float B;
        int i,id,nthrds;
        id=omp_get_thread_num();        //当前线程的ID
        nthrds=omp_get_num_threads();   //当前的线程个数
        for(i=id,i<niters;i+=thrds)     //巧妙的for循环,尽可能将循环任务平均地分配到各线程中去
        {
            B=big_job(i);
            #pragma omp critical
                consume(B,res);
        }
    }

2.原子操作

原子操作不会被多线程打断
然而原子操作和临界区的功能是一样的,因为有复合语句的存在,原子操作的功能实际上还要弱一些
原子操作中不能使用复合语句,也不能进行函数调用

    #pragma omp parallel
    {
        double tmp,B;
        B=DOIT();
        tmp=big_ugly(B);
        #pragma omp atomic
            X+=tmp;
    }
  • 提供原子操作的意义在于效率
    使用原子操作的效率,比使用临界区要高很多,因为可以调用一些系统底层的特殊功能来实现原子操作

3.路障同步

4.同步次序

5.flush

6.锁

四、并行循环

1.SPMD与worksharing

  • 工作共享创建了一个Single Program Multiple Data的程序结构
  • 使得多个线程以看起来一样的代码完成不同的工作

2.分配循环用的worksharing

    #pragma omp for
        for(i=0;i<N;i++)
        {
            something();
        }
  • i将自动地成为每个线程的私有变量
  • 默认得到{0,1,2,3},{4,5,6,7},...这样的循环划分方法
    可以调整,但无法任意划分

3.worksharing的结构特点

  • worksharing结构不会创建线程
    仅仅对执行做分配
  • worksharing结构在入口没有路障同步,但出口处有
    而且都是隐式的

4.worksharing结构的限制

  • 必须放在并行区内
  • 待分配的任务无法执行一部分,要么整个分配,要么不分配
  • 分配时有固定的次序,不支持自定义的次序
    也不会随机分配

5.worksharing结构的类型

  • section可以进行手动分配
  • single可以分配给单个线程

6.parallel与worksharing的组合

    double res[MAX];
    int i;
    #pragma omp parallel for
        for(i=0;i<MAX;i++)
            res[i]=huge();

7.规约

  • OpenMP提供的特殊、常见数据类型的支持

编译指导语句的基本格式

`#pragma omp directive-name [clause,...] newline`

规约指导语句

`reduction(op:list)`

归约操作的操作符和初始值

  • 由OpenMP规定
  • 无法自行定义

五、同步

1.Barrier

    #pragma omp barrier             //手动的路障同步
    #pragma omp for nowait          //指明取消末尾的隐式路障同步
  • 直到所有线程执行到此位置才继续执行
  • 离开临界区时有隐式的路障同步

2.Master结构

  • 标记一个代码块只被一个线程执行
  • 其它线程简单跳过
  • 默认没有路障同步,需要显式指定

3.Single结构

  • 此结构中的内容只有一个线程执行
  • 可能由任何一个线程执行,未必是master线程
  • 出口处有隐式的路障同步

4.ordered

  • 只加在for循环后
  • 表明for循环存在次序依赖
    标记出的语句将按照for循环的串行迭代序被执行
  • 对性能将产生很大的影响

5.锁

简单锁

可以认为是简单的布尔变量
omp_*_lock

  • init
  • set
  • unset
  • test
  • destroy

嵌套锁

与简单锁不同,可以被同一个进程反复地加锁,解锁时也要进行相应数量的解锁
omp_*_nest_lock

  • init
  • set
  • unset
  • test
  • destroy

简单锁的例子

    #include <omp.h>
    omp_lock_t lock;
    omp_init_lock(&lck);
    
    #pragma omp parallel private(tmp,id)
    {
        id=omp_get_thread_num();
        tmp=do_lots_of_work(id);
        omp_set_lock(&lock);
        omp_unset_lock(&lock);
    }
    omp_destroy_lock(&lock);

六、OpenMP的库函数

1.修改、设置线程数量

  • omp_set_num_threads(int)
  • omp_get_num_threads()
    获取此韩式调用时的线程数量
  • omp_get_thread_num()
    获取当前线程的线程号
  • omp_get_max_threads()
    获取下一个开辟的并行区每个线程要开启的线程数

2.是否在并行区域内

  • omp_in_parallel()

3.是否允许系统动态调整线程数量

  • omp_set_dynamic(int)
  • omp_get_dynamic()

4.系统处理器数量

  • omp_num_procs()

5.环境变量

环境变量的优先级比库函数要低一些

  • OMP_NUM_THREADS
  • OMP_SCHEDULE
    设置for循环是横切或竖切

七、数据环境

1.默认存储属性

  • 共享内存的编程模型
  • 全局变量在线程间共享
  • 静态变量是共享的
  • 堆内存是共享的
    动态分配的内存

默认情况下的私有变量

  • 并行区内定义的变量

2.private子句

  • 为变量创建每个线程一份的副本
  • 未经初始化的变量,在OpenMP中的初始值未被定义
    主流平台上,private变量的修改对外围没有改变
  • 外部变量作为私有变量,对定义为私有变量的变量的修改,修改谁并没有明确的定义
    实际平台上的主流编译器都修改全局变量

3.firstprivate与lastprivate子句

  • 和private子句几乎相同
  • firstprivate
    私有变量的初值定义为全局变量原先的值
  • lastprivate
    出并行区时,全局变量的值将被改变
    通常执行的最后一条更新的值反映到全局变量中

4.default子句

default(PRIVATE|SHARED|NONE)
  • default(SHARED)是默认存在的,因此不需写出来
    #pragma omp task除外
  • 在C中,default(PRIVATE)不被支持
  • default(NONE)将不为变量设定默认值
    此时必须为每个变量显式指定属性
    良好的自虐的编程实践~
    通常只在需要编译器提醒哪个变量没有指定属性时才使用

5.threadprivate子句

    int counter=0;
    #pragma omp threadprivate(counter)
  • 定义为threadprivate的变量是可以穿越多个并行区的
    变量的值以线程号一一对应

copyin子句

    int a=100;
    #pragma omp threadprivate copyin(a)
  • 可以将全局变量的值拷贝进对应的私有变量

copyprivate子句

  • 只能在single中使用
  • 在路障同步点处由执行single的线程拷贝到所有其它线程

指针的传递

  • 在线程之间,指针不要随便乱传
        #pragma omp parallel private(x) shared(p0,p1)
        x=...;
        p0=&x;
在另一个线程中使用p0指针会造成不可预料的后果

八、Schedule子句

1.section子句

    #pragma omp parallel
    {
        #pragma omp sections
        {
            #pragma omp section
            calculation1();
            #pragma omp section
            calculation2();
            #pragma omp section
            calculation3();
        }
    }
  • 这些任务由系统自由分配给不同线程运行
  • 任务数与线程数相等时,分配显然
  • 任务数多于线程数时
    先用任务把线程占满,哪个线程执行完在分配剩下的任务
  • 任务数少于线程数时
    其它线程等待

2.schedule子句

`schedule(mode[,chunk])`

实际上大多数编译器除了static,另外三种都没实现

静态调度

  • 所有分配方式在编码时写死
  • 默认的分配方式
  • chunk默认为最大值(迭代数/线程数)
    chunk是循环任务分块的大小
    如果需要循环纵切,chunk设置为1即可
  • 静态调度的分配方式是非常明确的,第一个chunk给线程0,以此类推

动态调度

  • 每个chunk可以动态分配给某个线程了

guided调度

  • chunk定义的是块的最小值
  • 实际上可以更大

runtime调度

  • 全部参数交由编译器决定

九、内存模型

1.弱一致性

  • 在代码中,读写顺序在不改变语义的情况下是可以改变的
  • 以S表示数据同步操作
    OpenMP中保证,S->W、S->R、R->S、W->S、S->S
    在OpenMP中就是flush操作

2.flush

    a=...;
    <other computaion>
    #pragma omp flush(a)
  • 变量值在内存中的改变最早发生在写操作,最晚在数据同步操作时进行

隐式数据同步

其它所有同步都会自动带上数据同步

十、OpenMP 3.0与任务

1.任务

  • 其它结构的工作量都是静态的,但task的任务是可以动态分的

2.例子

    for(int i=0;i<N;i+=a[i])
        task(a[i]);
  • 此循环不能使用#pragma omp for
  • 想要并行就必须使用task

3.task的结构

`#pragma omp task [clause[[,],clause]...]`
  • 子句可以加入ifuntitled与所有数据环境

并行的链表举例

    #pragma omp parallel
    {
        #pragma omp single private(p)   //由一个线程进行预处理,其它线程什么都不做
        {
            p=listhead;
            while(p)
            {
                #pragma omp task
                process(p);             //将链表内多个结点的处理并行进行,
                                        //占用并行区内原本闲置的线程
                p=next(p);
            }
        }
    }

4.untied子句

  • 创建的任务,默认将会与某个线程绑定,只能由某个线程来完成
  • untied可以用来解除这样的绑定

举例

    #pragma omp single
    {
        #pragma omp task untied
        for(i=0;i<ONEZILLION;i++)
            #pragma omp task
            process(item[i]);
    }
  • 如果不作为united的任务,源源不断的新任务将撑爆内存
  • untied允许任务的创建在其它线程间迁移

5.if子句

  • 如果表达式为false,整个编译指导语句无效
  • 默认为if(true)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容