Transcript chap4

第四章 OpenMP多线程编程
主要内容
1. OpenMP编程简介
2. OpenMP多线程应用程序编程技术
1. OpenMP编程简介
1.1 OpenMP多线程编程发展概况
• OpenMP是一种面向共享内存多线程并
行编程技术
• OpenMP具有良好的可移植性
• 支持多种编程语言 Fortran C/C++
• 支持多种平台
• www.openmp.org
• OpenMP最初是为共享内存的多处
理器系统设计的并行编程方法,这与
通过消息传递进行并行编程模型有很
大的不同。
OpenMP的支持环境
• Intel的C++和Fortran编译器
• Microsoft的Visual Studio 2005
• gcc4.2以上版本
1.2 OpenMP多线程编程基础
• OpenMP的编程模型以线程为基础,通过编
译制导语句来显示地指导并行化
• OpenMP的执行模型采用Fork-Join的形式,
在开始时,只有一个叫做主线程的运行线程存
在;在运行过程中,当遇到需要进行并行计算
的时候,派生出(Fork)线程来执行并行任
务;在并行代码结束执行,派生线程退出或挂
起,控制流程回到单独的主线程中(Join)
Fork-Join模型
并行区域
OpenMP的实现
• 编译制导语句(精髓)
• 运行时库函数
• 环境变量
编 译 指
导语句
运行时函
数库
环境变
量
编译制导语句
• 在编译器编译程序的时候,会识别特定的注释,
而这些注释就包含着OpenMP程序的一些语
义
• 在一个无法识别OpenMP语意的普通编译器
中,这些注释会被当作普通的注释而被忽略
• 在C/C++程序中,OpenMP所有编译制导语
句以#pragma omp开始,后面跟具体功能
指令
编译制导语句
#pragma omp
directive-name
制导指令前缀。 OpenMP制导
对所有的
指令。在制导
指令前缀和子
OpenMP语句
都需要这样的 句之间必须有
前缀。
一个正确的
OpenMP制导
指令.
[clause, ...]
子句。在没有
其它约束条件
下,子句可以
无序,也可以
任意的选择。
这一部分也可
以没有。
newline
换行符。表明
这条制导语句
的终止。
编译制导语句
• Directive
• parallel, for, parallel for, section,
sections, single, master, critical,
flush, ordered, atomic
运行时库函数
• OpenMP运行时函数库主要用以设置和
获取执行环境相关的信息,它们当中也
包含一系列用以同步的API
• 运行时函数库 “omp.h”
• omp_get_thread_num() 返回当前
线程的号码
• 通过编译制导语句,可以将串行的程序逐步地改
造成一个并行程序,达到增量更新程序的目的,
减少程序编写人员一定的负担.
• 串行程序和并行程序保持在同一个源代码文件
当中,减少了维护负担.
• 编译制导语句,优势体现在编译阶段
• 运行时库函数,支持运行时对并行环境的改变和
优化
1.3 编写OpenMP程序的准备
工作
• 当前的Visual Studio .Net 2005完
全支持OpenMP 2.0标准
• 通过新的编译器选项 /openmp来支持
OpenMP程序的编译和链接
建立一个新的项目
配置项目属性
设置环境变量
• 在OpenMP中,主要通过对循环
或一段结构化代码定义并行区域
的方式来实现多线程并行。
#include "omp.h"
int _tmain(int argc, _TCHAR* argv[])
{
printf("Hello from serial.\n");
printf("Thread
number=%d\n",omp_get_thread_num());
#pragma omp parallel
{
printf("Hello from parallel. Thread
number=%d\n", omp_get_thread_num());
}
printf("Hello from serial again.\n");
}
getchar();
return 0;
2. OpenMP多线程应用程序
编程技术
2.1 循环并行化
• 循环并行化是使用OpenMP来并行化程序的最重
要的部分
• 在C/C++语言中,循环并行化语句的编译制导语
句格式如下:
#pragma omp parallel for [clause[clause…]]
for( i = first ; i<last ; i++)
{
body of the loop;
}
• 另一种格式:
#pragma omp parallel [clause[clause…]]
{
#pragma omp for [clause[clause…]]
for( i = first ; i<last ; i++)
{
body of the loop;
}
}
• 如果并行的线程需要在循环的开始,或结束时作些工作的
话,就只能用parallel与for子句分离的版本。
• Parallel 将紧跟的程序块扩展为若干完全等同的并行区
域,每个线程拥有完全相同的并行区域;
• For 将循环中工作分配到线程组中,线程组中的每一个线
程完成循环中的一部分。
• 子句用来控制编译制导语句的具体行为。
循环并行化语句的限制
并行化的语句必须是for循环语句并具有规范格式
• 能够推测出循环的次数
• for (index = start ; index < end ;
increment_expr)
• 在循环过程中不能使用break语句
• 不能使用goto和return语句从循环中跳出
• 可以使用continue语句
简单循环并行化
• 各个分量之间没有数据相关性
• 循环计算的过程也没有循环依赖型
循环依赖性
• 循环迭代相关
循环迭代相关
循环分块技术创建无循环迭代相关的循环m
循环并行化编译制导语句的子句
• 循环并行化子句可以包含一个或者多个子句来
控制循环并行化的实际执行
• 常见子句有:
•
•
•
•
•
作用域子句(变量是共享的share还是私有的private)
控制线程的调度(schedule )子句
动态控制是否并行化(if )子句
进行同步的子句(ordered )子句
控制变量在串行部分与并行部分传递(copyin )
子句
循环嵌套
• 可以将嵌套循环的任意一个循环体进行并
行化
• 循环并行化编译指导语句可以加在任意一
个循环之前,则对应的最近的循环语句被
并行化,其它部分保持不变
控制数据的共享属性
• OpenMP程序在同一个共享内存空间上
执行
• 可以任意使用这个共享内存空间上的变
量进行线程间的数据传递
• OpenMP还允许线程保留自己的私有变
量不能让其它线程访问到
• 分配在栈上的数据都是
私有的
• 全局变量及代码是共享
的
• 动态分配的堆空间是共
享的
• threadprivate指明某
数据结构是私有的全局
变量
控制数据的共享属性
• shared用来指示一个变量的作用域是共享的。
• privare用来指示一个变量的作用域是私有的。
• firstprivate对私有变量进行初始化,把串行
变量值拷贝到私有变量中(线程开始)
• lastprivate对私有变量最后终结的操作,把
私有变量拷贝到同名串行变量中
使用作用域子句的一些规则
• 作用域子句中的变量是已经声明的有名变量
• 作用域子句在作用到类或者结构的时候,只能作用到类或
者结构的整体,而不能只作用域类或者结构的一个部分
• 一个编译指导语句能够包含多个数据作用域子句
• 作用域子句只能出现在编译制导语句起作用的语句变量部
分
• 默认情况下,并行区域中的所有变量是共享的,三种例外:
• parallel for 循环中,循环变量是私有的
• 并行区域中的局部变量是私有的
• Private,firstprivate,lastprivate,
reduction子句列出的变量是私有的
规约操作的并行化
• 在规约操作中,会反复将一个二元运算符应用
在一个变量和另外一个值上,并把结果保存在
原变量中
• 在使用规约操作时,只需在变量前指明规约操
作的类型以及规约的变量
# pragma omp parallel for private(arx,ary,n)
reduction(+:a,b)
for(i=0;i<n;i++){
a=a+arx[i];
b=b+ary[i];
}
规约操作并行化的限制
• 能够在OpenMP的C/C++语言中出现的规约操作
运算符
+
数据类型
整数,浮点
默认初始值
0
*
整数,浮点
1
-
整数,浮点
0
&
整数
所有位都开启,
~0
|
整数
0
^
整数
0
&&
整数
1
||
整数
0
私有变量的初始化和终结操作
• 循环并行化开始的时候,私有变量具有
主线程中的同名变量的值
• 循环并行化后将私有变量返回给主线程
中的同名变量
• firstprivate lastprivate
数据相关性与并行化操作
• 并不是所有的循环都能够使用#pragma
omp parallel for来进行并行化
• 必须要保证数据两次循环之间不存在数据
相关性
• 数据相关性又被称为数据竞争(Data
Race)
• 当两个线程对同一个变量进行操作,并且
有一个操作为写操作的时候,就说明这两
个线程存在数据竞争
2.2 并行区域编程
• 循环并行化实际上是并行区域编程的一个
特例
• 并行区域简单的说就是通过编译制导语句
使得一段代码能够在多个线程内部同时执
行
• 并行区域编写的格式如下:
#pragma omp parallel [clause[clause]…]
block
parallel编译制导语句的执行
过程
• 当程序遇到parallel编译制导语句的时
候,就会生成相应数目(根据环境变量)
的线程组成一个线程组,并将代码重复
地在各个线程内部执行
• parallel的末尾有一个隐含的同步屏障
(barrier),所有线程完成所需的重复
任务有,在这个同步屏障出会和(join)
线程私有数据
threadprivate,copyin
• threadprivate指明(全局)变量是线程
私有数据
• copyin对线程私有的全局变量进行初始
化
线程私有数据与
threadprivate,
copyin子句
工作队列
工作队列的基本工作过程:
• 为维持一个工作的队列,线程在并行执
行的时候,不断从这个队列中取出相应
的工作完成,直到队列为空为止
根据线程号分配任务
• 每一个线程在执行的过程中的线程标识
号是不同的,可以根据这个线程标识号
来分配不同的任务
#pragma omp parallel private(myid)
{
nthreads=omp_get_num_threads();
myid=omp_get_thread_num();
get_my_work_done(myid,nthreads);
}
使用循环语句分配任务
工作分区编码
2.3 线程同步
OpenMP支持两种不同类型的线程同步机
制:
• 互斥锁:可以用来保护一块共享的存储
空间,使得每一次访问这块共享内存空
间的线程最多一个,保证了数据的完整
性
• 事件通知:这种机制保证了多个线程之
间的执行顺序
数据竞争
互斥锁机制
• 用来对一块内存进行保护
• OpenMP提供了三种不同的互斥锁机
制:
• 临界区(critical)
• 原子操作(atomic)
• 由库函数来提供同步操作(互斥函数)
临界区(critical)
• 临界区通过编译指导语句对产生数据竞争的内
存变量进行保护
• 在程序需要访问可能产生竞争的内存数据的时
候,都需要插入相应的临界区代码
• #pragma omp critical [(name)]
block
• 在执行上述的程序块block之前,必须首先要
获得临界区的控制权
正整数组最大的元素
原子操作
• 现代体系结构的多处理计算机提供了原子更新
一个单一内存单元的方法,提供了一种更高效
率的互斥锁机制。
• 通过编译制导可以调用上述方法
• #pragma omp atomic
x ++
运行时库函数的互斥锁支持
函数名称
描述
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 *)
成功是返回真(true),失
败是返回假(false)
事件同步机制
• 锁:维护一块代码或者一块内存的一致性;
• 事件:用来控制代码的执行顺序,使得某一部
分代码必须在其它的代码执行完毕之后才能执
行。
• OpenMP中的事件同步主要包括:
• 同步屏障(barrier)
• 定序区段(ordered sections)
• 主线程执行(master)
隐含的同步屏障(barrier)
• 在每一个并行区域都会有一个隐含的同步屏障
• 一个同步屏障要求所有的线程执行到此屏障,然
后才能够继续执行下面的代码
• #pragma omp for,#pragma omp
single, #pragma omp sections程序块
都包含自己的隐含的同步屏障
• 为了避免在循环过程中不必要的同步屏障,可以
增加nowait子句到相应的编译指导语句中
明确的同步屏障语句
• 在有些情况下,隐含的同步屏障并不能提供有
效的同步措施
• 程序员可以在需要的地方插入明确的同步屏障
语句#pragma omp barrier
• 在并行区域的执行过程中,所有的执行线程都
会在同步屏障语句上进行同步
• #pragma omp parallel
{
}
initialization();
#pragma omp barrier
process();
循环并行化中的顺序语句
(ordered)
• 对于循环并行化中的某些处理需要规定
执行的顺序
• 典型的情况:在一次循环的过程中
• 一大部分的工作是可以并行执行的,而其余
的工作需要等到前面的工作全部完成之后才
能够执行
• 在循环并行化的过程中,可以使用
ordered子句使得顺序执行的语句直到
前面的循环都执行完毕之后再执行
OpenMP任务调度
• OpenMP中,任务调度主要用于并行的
for循环中,当循环中每次迭代的计算量
不相等时,如果简单地给各个线程分配
相同次数的迭代的话,会造成各个线程
计算负载不均衡,这会使得有些线程先
执行完,有些后执行完,造成某些CPU
核空闲,影响程序性能。
int i, j;
int a[100][100] = {0};
for ( i =0; i < 100; i++)
{
for( j = i; j < 100; j++ )
{
a[i][j] = i*j;
}
}
如果将最外层循环并行化的话,比如使用4个
线程,如果给每个线程平均分配25次循环迭代
计算的话,显然i=0和i=99的计算量相差了
100倍,那么各个线程间可能出现较大的负载
不平衡情况。
for循环并行化的任务调度方案
schedule子句的使用格式为:
schedule(type[,size])
1. type参数
表示调度类型,有四种调度类型如下:
· static
· dynamic
· guided
· runtime (实际上是根据环境变量来选择前三种中的某
中类型。)
2. size参数 (可选)
size参数表示循环迭代次数,size参数必须是整数。
静态调度(static)
• 当parallel for编译制导语句没有带
schedule子句时,大部分系统中默认
采用static调度方式,这种调度方式非
常简单。假设有n次循环迭代,t个线程,
那么给每个线程静态分配大约n/t次迭代
计算。
• 不使用size参数时,分配给每个线程的
是n/t次连续的迭代
#pragma omp parallel for schedule(static)
for(i = 0; i < 10; i++ )
{
printf("i=%d, thread_id=%d\n", i,
omp_get_thread_num());
}
i=0, thread_id=0
i=1, thread_id=0
i=2, thread_id=0
i=3, thread_id=0
i=4, thread_id=0
i=5, thread_id=1
i=6, thread_id=1
i=7, thread_id=1
i=8, thread_id=1
i=9, thread_id=1
可以看出线程0得到了0~4次连续迭代,线程1得到5~9次连续迭代。
#pragma omp parallel for schedule(static, 2)
for(i = 0; i < 10; i++ )
{
printf("i=%d, thread_id=%d\n", i,
omp_get_thread_num());
}
i=0, thread_id=0
i=1, thread_id=0
i=4, thread_id=0
i=5, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=2, thread_id=1
i=3, thread_id=1
i=6, thread_id=1
i=7, thread_id=1
0、1次迭代分配给线程0,每个线程依次分配到2次连续的迭代计算。
动态调度(dynamic)
• 动态调度是动态地将迭代分配到各个线程,动
态调度可以使用size参数也可以不使用size参
数,不使用size参数时是将迭代逐个地分配到
各个线程,使用size参数时,每次分配给线程
的迭代次数为指定的size次。
• 使用一个内部任务队列,采用先来先服务的方
式进行调度。当某个线程闲下来,就为其分配
一个循环块。由此可见,动态策略可以极大地
保证线程组的负载平衡,但是需要额外的开销,
不能达到最佳性能。
guided调度(guided)
• guided调度是一种采用指导性的启发式
自调度方法。开始时每个线程会分配到
较大的迭代块,之后分配到的迭代块会
逐渐递减。迭代块的大小会按指数级下
降到指定的size大小,如果没有指定
size参数,那么迭代块大小最小会降到1。
runtime调度(rumtime)
• runtime调度并不是和前面三种调度方式似的
真实调度方式,它是在运行时根据环境变量
OMP_SCHEDULE来确定调度类型
• 例如在unix系统中,可以使用setenv命令来
设置OMP_SCHEDULE环境变量:
setenv OMP_SCHEDULE “dynamic, 2”
上述命令设置调度类型为动态调度,动态调度的
迭代次数为2。
• 在windows环境中,可以在”系统属性|高级|
环境变量”对话框中进行设置环境变量。