openmp并行程序设计.docx
《openmp并行程序设计.docx》由会员分享,可在线阅读,更多相关《openmp并行程序设计.docx(45页珍藏版)》请在冰点文库上搜索。
openmp并行程序设计
OpenMP并行程序设计
(一)
OpenMP是一个支持共享存储并行设计的库,特别适宜多核CPU上的并行程序设计。
今天在双核CPU机器上试了一下OpenMP并行程序设计,发现效率方面超出想象,因此写出来分享给大家。
在VC8.0中项目的属性对话框中,左边框里的“配置属性”下的“C/C++”下的“语言”页里,将OpenMP支持改为“是/(OpenMP)”就可以支持OpenMP了。
先看一个简单的使用了OpenMP程序
intmain(intargc,char*argv[])
{
#pragmaompparallelfor
for(inti=0;i<10;i++)
{
printf("i=%d\n",i);
}
return0;
}
这个程序执行后打印出以下结果:
i=0
i=5
i=1
i=6
i=2
i=7
i=3
i=8
i=4
i=9
可见for循环语句中的内容被并行执行了。
(每次运行的打印结果可能会有区别)
这里要说明一下,#pragmaompparallelfor这条语句是用来指定后面的for循环语句变成并行执行的,当然for循环里的内容必须满足可以并行执行,即每次循环互不相干,后一次循环不依赖于前面的循环。
有关#pragmaompparallelfor这条语句的具体含义及相关OpenMP指令和函数的介绍暂时先放一放,只要知道这条语句会将后面的for循环里的内容变成并行执行就行了。
将for循环里的语句变成并行执行后效率会不会提高呢,我想这是我们最关心的内容了。
下面就写一个简单的测试程序来测试一下:
voidtest()
{
inta=0;
clock_tt1=clock();
for(inti=0;i<0;i++)
{
a=i+1;
}
clock_tt2=clock();
printf("Time=%d\n",t2-t1);
}
intmain(intargc,char*argv[])
{
clock_tt1=clock();
#pragmaompparallelfor
for(intj=0;j<2;j++){
test();
}
clock_tt2=clock();
printf("Totaltime=%d\n",t2-t1);
test();
return0;
}
在test()函数中,执行了1亿次循环,主要是用来执行一个长时间的操作。
在main()函数里,先在一个循环里调用test()函数,只循环2次,我们还是看一下在双核CPU上的运行结果吧:
Time=297
Time=297
Totaltime=297
Time=297
可以看到在for循环里的两次test()函数调用都花费了297ms,但是打印出的总时间却只花费了297ms,后面那个单独执行的test()函数花费的时间也是297ms,可见使用并行计算后效率提高了整整一倍。
下一篇文章中将介绍OpenMP的具体指令和用法。
OpenMP并行程序设计
(二)
1、fork/join并行执行模式的概念
OpenMP是一个编译器指令和库函数的集合,主要是为共享式存储计算机上的并行程序设计使用的。
前面一篇文章中已经试用了OpenMP的一个Parallelfor指令。
从上篇文章中我们也可以发现OpenMP并行执行的程序要全部结束后才能执行后面的非并行部分的代码。
这就是标准的并行模式fork/join式并行模式,共享存储式并行程序就是使用fork/join式并行的。
标准并行模式执行代码的基本思想是,程序开始时只有一个主线程,程序中的串行部分都由主线程执行,并行的部分是通过派生其他线程来执行,但是如果并行部分没有结束时是不会执行串行部分的,如上一篇文章中的以下代码:
intmain(intargc,char*argv[])
{
clock_tt1=clock();
#pragmaompparallelfor
for(intj=0;j<2;j++){
test();
}
clock_tt2=clock();
printf("Totaltime=%d\n",t2-t1);
test();
return0;
}
在没有执行完for循环中的代码之前,后面的clock_tt2=clock();这行代码是不会执行的,如果和调用线程创建函数相比,它相当于先创建线程,并等待线程执行完,所以这种并行模式中在主线程里创建的线程并没有和主线程并行运行。
2、OpenMP指令和库函数介绍
下面来介绍OpenMP的基本指令和常用指令的用法,
在C/C++中,OpenMP指令使用的格式为
#pragmaomp指令[子句[子句]…]
前面提到的parallelfor就是一条指令,有些书中也将OpenMP的“指令”叫做“编译指导语句”,后面的子句是可选的。
例如:
#pragmaompparallelprivate(i,j)
parallel就是指令,private是子句
为叙述方便把包含#pragma和OpenMP指令的一行叫做语句,如上面那行叫parallel语句。
OpenMP的指令有以下一些:
parallel,用在一个代码段之前,表示这段代码将被多个线程并行执行
for,用于for循环之前,将循环分配到多个线程中并行执行,必须保证每次循环之间无相关性。
parallelfor,parallel和for语句的结合,也是用在一个for循环之前,表示for循环的代码将被多个线程并行执行。
sections,用在可能会被并行执行的代码段之前
parallelsections,parallel和sections两个语句的结合
critical,用在一段代码临界区之前
single,用在一段只被单个线程执行的代码段之前,表示后面的代码段将被单线程执行。
flush,
barrier,用于并行区内代码的线程同步,所有线程执行到barrier时要停止,直到所有线程都执行到barrier时才继续往下执行。
atomic,用于指定一块内存区域被制动更新
master,用于指定一段代码块由主线程执行
ordered,用于指定并行区域的循环按顺序执行
threadprivate,用于指定一个变量是线程私有的。
OpenMP除上述指令外,还有一些库函数,下面列出几个常用的库函数:
omp_get_num_procs,返回运行本线程的多处理机的处理器个数。
omp_get_num_threads,返回当前并行区域中的活动线程个数。
omp_get_thread_num,返回线程号
omp_set_num_threads,设置并行执行代码时的线程个数
omp_init_lock,初始化一个简单锁
omp_set_lock,上锁操作
omp_unset_lock,解锁操作,要和omp_set_lock函数配对使用。
omp_destroy_lock,omp_init_lock函数的配对操作函数,关闭一个锁
OpenMP的子句有以下一些
private,指定每个线程都有它自己的变量私有副本。
firstprivate,指定每个线程都有它自己的变量私有副本,并且变量要被继承主线程中的初值。
lastprivate,主要是用来指定将线程中的私有变量的值在并行处理结束后复制回主线程中的对应变量。
reduce,用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的运算。
nowait,忽略指定中暗含的等待
num_threads,指定线程的个数
schedule,指定如何调度for循环迭代
shared,指定一个或多个变量为多个线程间的共享变量
ordered,用来指定for循环的执行要按顺序执行
copyprivate,用于single指令中的指定变量为多个线程的共享变量
copyin,用来指定一个threadprivate的变量的值要用主线程的值进行初始化。
default,用来指定并行处理区域内的变量的使用方式,缺省是shared
3、parallel指令的用法
parallel是用来构造一个并行块的,也可以使用其他指令如for、sections等和它配合使用。
在C/C++中,parallel的使用方法如下:
#pragmaompparallel[for|sections][子句[子句]…]
{
//代码
}
parallel语句后面要跟一个大括号对将要并行执行的代码括起来。
voidmain(intargc,char*argv[]){
#pragmaompparallel
{
printf(“Hello,World!
\n”);
}
}
执行以上代码将会打印出以下结果
Hello,World!
Hello,World!
Hello,World!
Hello,World!
可以看得出parallel语句中的代码被执行了四次,说明总共创建了4个线程去执行parallel语句中的代码。
也可以指定使用多少个线程来执行,需要使用num_threads子句:
voidmain(intargc,char*argv[]){
#pragmaompparallelnum_threads(8)
{
printf(“Hello,World!
ThreadId=%d\n”,omp_get_thread_num());
}
}
执行以上代码,将会打印出以下结果:
Hello,World!
ThreadId=2
Hello,World!
ThreadId=6
Hello,World!
ThreadId=4
Hello,World!
ThreadId=0
Hello,World!
ThreadId=5
Hello,World!
ThreadId=7
Hello,World!
ThreadId=1
Hello,World!
ThreadId=3
从ThreadId的不同可以看出创建了8个线程来执行以上代码。
所以parallel指令是用来为一段代码创建多个线程来执行它的。
parallel块中的每行代码都被多个线程重复执行。
和传统的创建线程函数比起来,相当于为一个线程入口函数重复调用创建线程函数来创建线程并等待线程执行完。
4、for指令的使用方法
for指令则是用来将一个for循环分配到多个线程中执行。
for指令一般可以和parallel指令合起来形成parallelfor指令使用,也可以单独用在parallel语句的并行块中。
#pragmaomp[parallel]for[子句]
for循环语句
先看看单独使用for语句时是什么效果:
intj=0;
#pragmaompfor
for(j=0;j<4;j++){
printf(“j=%d,ThreadId=%d\n”,j,omp_get_thread_num());
}
执行以上代码后打印出以下结果
j=0,ThreadId=0
j=1,ThreadId=0
j=2,ThreadId=0
j=3,ThreadId=0
从结果可以看出四次循环都在一个线程里执行,可见for指令要和parallel指令结合起来使用才有效果:
如以下代码就是parallel和for一起结合成parallelfor的形式使用的:
intj=0;
#pragmaompparallelfor
for(j=0;j<4;j++){
printf(“j=%d,ThreadId=%d\n”,j,omp_get_thread_num());
}
执行后会打印出以下结果:
j=0,ThreadId=0
j=2,ThreadId=2
j=1,ThreadId=1
j=3,ThreadId=3
可见循环被分配到四个不同的线程中执行。
上面这段代码也可以改写成以下形式:
intj=0;
#pragmaompparallel
{
#pragmaompfor
for(j=0;j<4;j++){
printf(“j=%d,ThreadId=%d\n”,j,omp_get_thread_num());
}
}
执行以上代码会打印出以下结果:
j=1,ThreadId=1
j=3,ThreadId=3
j=2,ThreadId=2
j=0,ThreadId=0
在一个parallel块中也可以有多个for语句,如:
intj;
#pragmaompparallel
{
#pragmaompfor
for(j=0;j<100;j++){
…
}
#pragmaompfor
for(j=0;j<100;j++){
…
}
…
}
for循环语句中,书写是需要按照一定规范来写才可以的,即for循环小括号内的语句要按照一定的规范进行书写,for语句小括号里共有三条语句
for(i=start;ii=start;是for循环里的第一条语句,必须写成“变量=初值”的方式。
如i=0
i变量<边界值
变量<=边界值
变量>边界值
变量>=边界值
如i>10i<10i>=10i>10等等
最后一条语句i++可以有以下9种写法之一
i++
++i
i--
--i
i+=inc
i-=inc
i=i+inc
i=inc+i
i=i–inc
例如i+=2;i-=2;i=i+2;i=i-2;都是符合规范的写法。
5sections和section指令的用法
section语句是用在sections语句里用来将sections语句里的代码划分成几个不同的段,每段都并行执行。
用法如下:
#pragmaomp[parallel]sections[子句]
{
#pragmaompsection
{
代码块
}
}
先看一下以下的例子代码:
voidmain(intargc,char*argv)
{
#pragmaompparallelsections{
#pragmaompsection
printf(“section1ThreadId=%d\n”,omp_get_thread_num());
#pragmaompsection
printf(“section2ThreadId=%d\n”,omp_get_thread_num());
#pragmaompsection
printf(“section3ThreadId=%d\n”,omp_get_thread_num());
#pragmaompsection
printf(“section4ThreadId=%d\n”,omp_get_thread_num());
}
执行后将打印出以下结果:
section1ThreadId=0
section2ThreadId=2
section4ThreadId=3
section3ThreadId=1
从结果中可以发现第4段代码执行比第3段代码早,说明各个section里的代码都是并行执行的,并且各个section被分配到不同的线程执行。
使用section语句时,需要注意的是这种方式需要保证各个section里的代码执行时间相差不大,否则某个section执行时间比其他section过长就达不到并行执行的效果了。
上面的代码也可以改写成以下形式:
voidmain(intargc,char*argv)
{
#pragmaompparallel{
#pragmaompsections
{
#pragmaompsection
printf(“section1ThreadId=%d\n”,omp_get_thread_num());
#pragmaompsection
printf(“section2ThreadId=%d\n”,omp_get_thread_num());
}
#pragmaompsections
{
#pragmaompsection
printf(“section3ThreadId=%d\n”,omp_get_thread_num());
#pragmaompsection
printf(“section4ThreadId=%d\n”,omp_get_thread_num());
}
}
执行后将打印出以下结果:
section1ThreadId=0
section2ThreadId=3
section3ThreadId=3
section4ThreadId=1
这种方式和前面那种方式的区别是,两个sections语句是串行执行的,即第二个sections语句里的代码要等第一个sections语句里的代码执行完后才能执行。
用for语句来分摊是由系统自动进行,只要每次循环间没有时间上的差距,那么分摊是很均匀的,使用section来划分线程是一种手工划分线程的方式,最终并行性的好坏得依赖于程序员。
本篇文章中讲的几个OpenMP指令parallel,for,sections,section实际上都是用来如何创建线程的,这种创建线程的方式比起传统调用创建线程函数创建线程要更方便,并且更高效。
当然,创建线程后,线程里的变量是共享的还是其他方式,主线程中定义的变量到了并行块内后还是和传统创建线程那种方式一样的吗?
创建的线程是如何调度的?
等等诸如此类的问题到下一篇文章中进行讲解
OpenMP中的数据处理子句
10.1.1 private子句
private子句用于将一个或多个变量声明成线程私有的变量,变量声明成私有变量后,指定每个线程都有它自己的变量私有副本,其他线程无法访问私有副本。
即使在并行区域外有同名的共享变量,共享变量在并行区域内不起任何作用,并且并行区域内不会操作到外面的共享变量。
private子句的用法格式如下:
private(list)
下面便是一个使用private子句的代码例子:
int k=100;
#pragma ompparallel for private(k)
for (k=0;k<10;k++)
{
printf("k=%d\n",k);
}
printf("lastk=%d\n",k);
上面程序执行后打印的结果如下:
k=6
k=7
k=8
k=9
k=0
k=1
k=2
k=3
k=4
k=5
lastk=100
从打印结果可以看出,for循环前的变量k和循环区域内的变量k其实是两个不同的变量。
用private子句声明的私有变量的初始值在并行区域的入口处是未定义的,它并不会继承同名共享变量的值。
出现在reduction子句中的参数不能出现在private子句中。
10.1.2 firstprivate子句
private声明的私有变量不能继承同名变量的值,但实际情况中有时需要继承原有共享变量的值,OpenMP提供了firstprivate子句来实现这个功能。
先看一下以下的代码例子
int k=100;
#pragma ompparallel forfirstprivate(k)
for (i=0;i<4;i++)
{
k+=i;
printf("k=%d\n",k);
}
printf("lastk=%d\n",k);
上面代码执行后打印结果如下:
k=100
k=101
k=103
k=102
lastk=100
从打印结果可以看出,并行区域内的私有变量k继承了外面共享变量k的值100作为初始值,并且在退出并行区域后,共享变量k的值保持为100未变。
10.1.3 lastprivate子句
有时在并行区域内的私有变量的值经过计算后,在退出并行区域时,需要将它的值赋给同名的共享变量,前面的private和firstprivate子句在退出并行区域时都没有将私有变量的最后取值赋给对应的共享变量,lastprivate子句就是用来实现在退出并行区域时将私有变量的值赋给共享变量。
举个例子如下:
int k=100;
#pragma ompparallel forfirstprivate(k),lastprivate(k)
for (i=0;i<4;i++)
{
k+=i;
printf("k=%d\n",k);
}
printf("lastk=%d\n",k);
上面代码执行后的打印结果如下:
k=100
k=101
k=103
k=102
lastk=103
从打印结果可以看出,退出for循环的并行区域后,共享变量k的值变成了103,而不是保持原来的100不变。
由于在并行区域内是多个线程并行执行的,最后到底是将那个线程的最终计算结果赋给了对应的共享变量呢?
OpenMP规范中指出,如果是循环迭代,那么是将最后一次循环迭代中的值赋给对应的共享变量;如果是section构造,那么是最后一个section语句中的值赋给对应的共享变量。
注意这里说的最后一个section是指程序语法上的最后一个,而不是实际运行时的最后一个运行完的。
如果是类(class)类型的变量使用在lastprivate参数中,那么使用时有些限制,需要一个可访问的,明确的缺省构造函数,除非变量也被使用作为firstprivate子句的参数;还需要一个拷贝赋值操作符,并且这个