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倍,那么各个线程间可能出现较大的负载不平衡情况。为了解决这些问题,OpenMP中提供了几种对for循环并行化的任务调度方案。
在OpenMP中,对for循环并行化的任务调度使用schedule子句来实现,下面介绍schedule字句的用法。
schedule子句的使用格式为:
schedule(type[,size])
schedule有两个参数:type和size,size参数是可选的。
1.         type参数
表示调度类型,有四种调度类型如下:
·  dynamic
·  guided
·  runtime
·  static
这四种调度类型实际上只有static、dynamic、guided三种调度方式,runtime实际上是根据环境变量来选择前三种中的某中类型。
run-sched-var
2.         size 参数 (可选)
size 参数表示循环迭代次数, size 参数 必须是整数。static、dynamic、guided三种调度方式都可以使用size参数,也可以不使用size参数。 type 参数类型为 runtime 时, size 参数是非法的(不需要使用,如果使用的话编译器会报错)。
当parallel for编译指导语句没有带schedule子句时,大部分系统中默认采用static调度方式,这种调度方式非常简单。假设有n次循环迭代,t个线程,那么给每个线程静态分配大约n/t次迭代计算。这里为什么说大约分配n/t次呢?因为n/t不一定是整数,因此实际分配的迭代次数可能存在差1的情况,如果指定了size参数的话,那么可能相差一个size。
静态调度时可以不使用size参数,也可以使用size参数。
3.        不使用size参数
不使用size参数时,分配给每个线程的是n/t次连续的迭代,不使用size参数的用法如下:
schedule(static)
例如以下代码:
    #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次连续迭代。注意由于多线程执行时序的随机性,每次执行时打印的结果顺序可能存在差别,后面的例子也一样。
4.        使用size参数
使用size参数时,分配给每个线程的size次连续的迭代计算,用法如下:
schedule(static, size)
例如以下代码:
    #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、3次迭代分配给线程1,4、5次迭代分配给线程0,6、7次迭代分配给线程1,…。每个线程依次分配到2次连续的迭代计算。
动态调度是动态地将迭代分配到各个线程,动态调度可以使用size参数也可以不使用size参数,不使用size参数时是将迭代逐个地分配到各个线程,使用size参数时,每次分配给线程的迭代次数为指定的size次。
下面为使用动态调度不带size参数的例子:
          #pragma omp parallel  for schedule(dynamic)
          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=1
i=2, thread_id=0
i=3, thread_id=1
i=5, thread_id=1
i=6, thread_id=1
i=7, thread_id=1
i=8, thread_id=1
i=4, thread_id=0
i=9, thread_id=1
 
下面为动态调度使用size参数的例子:
          #pragma omp parallel  for schedule(dynamic, 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=2, thread_id=1
i=5, thread_id=0
i=3, thread_id=1
i=6, thread_id=0
i=8, thread_id=1
i=7, thread_id=0
i=9, thread_id=1
从打印结果可以看出第0、1,4、5,6、7次迭代被分配给了线程0,第2、3,8、9次迭代则分配给了线程1,每次分配的迭代次数为2。
1.1.4       guided调度(guided)
guided调度是一种采用指导性的启发式自调度方法。开始时每个线程会分配到较大的迭代块,之后分配到的迭代块会逐渐递减。迭代块的大小会按指数级下降到指定的size大小,如果没有指定size参数,那么迭代块大小最小会降到1。
例如以下代码:
    #pragma omp parallel for schedule(guided,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=2, thread_id=0
i=3, thread_id=0
i=4, thread_id=0
i=8, thread_id=0
i=9, thread_id=0
i=5, thread_id=1
i=6, thread_id=1
i=7, thread_id=1
第0、1、2、3、4次迭代被分配给线程0,第5、6、7次迭代被分配给线程1,第8、9次迭代被分配给线程0,分配的迭代次数呈递减趋势,最后一次递减到2次。
1.1.5       runtime调度(rumtime)
runtime调度并不是和前面三种调度方式似的真实调度方式,它是在运行时根据环境变量OMP_SCHEDULE来确定调度类型,最终使用的调度类型仍然是上述三种调度方式中的某种。
例如在unix系统中,可以使用setenv命令来设置OMP_SCHEDULE环境变量:
setenv OMP_SCHEDULE “dynamic, 2”
上述命令设置调度类型为动态调度,动态调度的迭代次数为2。
在windows环境中,可以在”系统属性|高级|环境变量”对话框中进行设置环境变量。
1、fork/join并行执行模式的概念
OpenMP是一个编译器指令和库函数的集合,主要是为共享式存储计算机上的并行程序设计使用的。
前面一篇文章中已经试用了OpenMP的一个Parallel for指令。从上篇文章中我们也可以发现OpenMP并行执行的程序要全部结束后才能执行后面的非并行部分的代码。这就是标准的并行模式fork/join式并行模式,共享存储式并行程序就是使用fork/join式并行的。
标准并行模式执行代码的基本思想是,程序开始时只有一个主线程,程序中的串行部分都由主线程执行,并行的部分是通过派生其他线程来执行,但是如果并行部分没有结束时是不会执行串行部分的,如上一篇文章中的以下代码:
int  main(int argc, char* argv[])
{
     clock_t t1 = clock();
#pragma  omp parallel for
     for ( int j = 0; j < 2; j++ ){
         test();
     }
     clock_t t2 = clock();
     printf("Total time = %d/n", t2-t1);
 
     test();
     return 0;
}
在没有执行完for循环中的代码之前,后面的clock_t t2 = clock();这行代码是不会执行的,如果和调用线程创建函数相比,它相当于先创建线程,并等待线程执行完,所以这种并行模式中在主线程里创建的线程并没有和主线程并行运行。
2、OpenMP指令和库函数介绍
下面来介绍OpenMP的基本指令和常用指令的用法,
在C/C++中,OpenMP指令使用的格式为
       pragma omp 指令 [子句[子句]…]
前面提到的parallel for就是一条指令,有些书中也将OpenMP的“指令”叫做“编译指导语句”,后面的子句是可选的。例如:
#pragma omp parallel private(i, j)
parallel 就是指令, private是子句
为叙述方便把包含#pragma和OpenMP指令的一行叫做语句,如上面那行叫parallel语句。
 
OpenMP的指令有以下一些:
        parallel,用在一个代码段之前,表示这段代码将被多个线程并行执行
        for,用于for循环之前,将循环分配到多个线程中并行执行,必须保证每次循环之间无相关性。
        parallel for, parallel 和 for语句的结合,也是用在一个for循环之前,表示for循环的代码将被多个线程并行执行。
        sections,用在可能会被并行执行的代码段之前
        parallel sections,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的使用方法如下:
#pragma omp parallel [for | sections] [子句[子句]…]
{
       //代码
}
parallel语句后面要跟一个大括号对将要并行执行的代码括起来。
void main(int argc, char *argv[]) {
#pragma omp parallel
{
              printf(“Hello, World!/n”);
}
}
执行以上代码将会打印出以下结果
Hello, World!
Hello, World!
Hello, World!
Hello, World!
可以看得出parallel语句中的代码被执行了四次,说明总共创建了4个线程去执行parallel语句中的代码。
也可以指定使用多少个线程来执行,需要使用num_threads子句:
void main(int argc, char *argv[]) {
#pragma omp parallel num_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指令合起来形成parallel for指令使用,也可以单独用在parallel语句的并行块中。
#pragma omp [parallel] for [ 子句 ]
      for 循环语句
 
先看看单独使用for语句时是什么效果:
int j = 0;
#pragma  omp for
     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一起结合成parallel for的形式使用的:
int j = 0;
#pragma  omp parallel for
     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
可见循环被分配到四个不同的线程中执行。
 
上面这段代码也可以改写成以下形式:
int j = 0;
#pragma  omp parallel
{
#pragma  omp for
     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语句,如:
int j;
#pragma  omp parallel
{
#pragma  omp for
     for ( j = 0; j < 100; j++ ){
         
     }
#pragma  omp for
     for (  j = 0; j < 100; j++ ){
         
     }
}
 
for 循环语句中,书写是需要按照一定规范来写才可以的,即for循环小括号内的语句要按照一定的规范进行书写,for语句小括号里共有三条语句
for( i=start; i < end; i++)
 
i=start; 是for循环里的第一条语句,必须写成 “变量=初值” 的方式。如 i=0
i < end;是for循环里的第二条语句,这个语句里可以写成以下4种形式之一:
变量 < 边界值
变量 <= 边界值
变量 > 边界值
变量 >= 边界值
如 i>10 i< 10   i>=10 i>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;都是符合规范的写法。
5 sections和section指令的用法
section语句是用在sections语句里用来将sections语句里的代码划分成几个不同的段,每段都并行执行。用法如下:
#pragma omp [parallel] sections [ 子句 ]
{
   #pragma omp section
   {
             代码块
   } 
}
 
先看一下以下的例子代码:
void main(int argc, char *argv)
{
#pragma omp parallel sections {
#pragma omp section
    printf(“section 1 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
    printf(“section 2 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
    printf(“section 3 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
    printf(“section 4 ThreadId = %d/n”, omp_get_thread_num());
}
执行后将打印出以下结果:
section 1 ThreadId = 0
section 2 ThreadId = 2
section 4 ThreadId = 3
section 3 ThreadId = 1
 
从结果中可以发现第4段代码执行比第3段代码早,说明各个section里的代码都是并行执行的,并且各个section被分配到不同的线程执行。
 
使用section语句时,需要注意的是这种方式需要保证各个section里的代码执行时间相差不大,否则某个section执行时间比其他section过长就达不到并行执行的效果了。
 
上面的代码也可以改写成以下形式:
void main(int argc, char *argv)
{
#pragma omp parallel {
#pragma omp sections
{
#pragma omp section
       printf(“section 1 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
       printf(“section 2 ThreadId = %d/n”, omp_get_thread_num());
}
#pragma omp sections
{
 
#pragma omp section
       printf(“section 3 ThreadId = %d/n”, omp_get_thread_num());
#pragma omp section
       printf(“section 4 ThreadId = %d/n”, omp_get_thread_num());
}
}
执行后将打印出以下结果:
section 1 ThreadId = 0
section 2 ThreadId = 3
section 3 ThreadId = 3
section 4 ThreadId = 1
 
这种方式和前面那种方式的区别是,两个sections语句是串行执行的,即第二个sections语句里的代码要等第一个sections语句里的代码执行完后才能执行。
 
用for语句来分摊是由系统自动进行,只要每次循环间没有时间上的差距,那么分摊是很均匀的,使用section来划分线程是一种手工划分线程的方式,最终并行性的好坏得依赖于程序员。
 
本篇文章中讲的几个OpenMP指令parallel, for, sections, section实际上都是用来如何创建线程的,这种创建线程的方式比起传统调用创建线程函数创建线程要更方便,并且更高效。
当然,创建线程后,线程里的变量是共享的还是其他方式,主线程中定义的变量到了并行块内后还是和传统创建线程那种方式一样的吗?创建的线程是如何调度的?等等诸如此类的问题到下一篇文章中进行讲解。
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值