• openMP编程(下篇)之数据私有与任务调度



    编码环境 :服务器【linux】.secureCRT

    openMP并行编程中数据的共享和私有

    OpenMP是共享内存的编程环境。在实际并行编程中需要将数据进行共享化或者私有化。

    OpenMP提供了一系列的子句来对共享和私有进行控制,常见的子句 :

    private :

    用于将一个或多个变量声明成线程私有变量,每个线程都有自己的私有变量副本,其他线程无法访问,即使在并行区域外有同名的共享变量,共享变量在并行区域内也不起作用,当然,并行区域内的私有变量在区域外也不起作用。

    #include <stdio.h>
    #include <omp.h>
    #include <stdlib.h>
    main (int argc, char **argv)
    {
      int i ;
      int k=100 ;  //定义共享变量
      printf ("first k = %d, addr=%x\n",  k, &i);
      omp_set_num_threads (4);
      #pragma omp parallel for private(k)  //将k变成私有变量
        for (i = 0; i <= 10; i++)
        { 
            //在并行区域内,k作为私有变量
            k = k+1 ;  
            printf ("I am thread %d,i = %d,k=%d, addr =%x\n", omp_get_thread_num (), i,k,&k);
        }
      printf ("last k = %d, addr=%x\n",  k, &k);
    }
    
    first k = 100, addr=debd1e28
    I am thread 0,i = 0,k=1, addr =debd1ddc
    I am thread 3,i = 9,k=32558, addr =59db9dec
    I am thread 3,i = 10,k=32559, addr =59db9dec
    I am thread 1,i = 3,k=32558, addr =5b1bbdec
    I am thread 1,i = 4,k=32559, addr =5b1bbdec
    I am thread 1,i = 5,k=32560, addr =5b1bbdec
    I am thread 2,i = 6,k=32558, addr =5a7badec
    I am thread 2,i = 7,k=32559, addr =5a7badec
    I am thread 2,i = 8,k=32560, addr =5a7badec
    I am thread 0,i = 1,k=2, addr =debd1ddc
    I am thread 0,i = 2,k=3, addr =debd1ddc
    last k = 100, addr=debd1e24
    

    从结果来看,相同线程的k变量的地址都是一样的,说明每个线程都有一个私有的k变量。而且并行区域中k变量的值并没有继承共享变量的初值100.

    firstprivate :

    注意到private中,结果的k的值乱七八糟,并不是初值100,有时候我们需要private声明的私有变量继承共享变量的初值,firstprivate就是处理这个问题的

      int i ;
      int k =100 ;
      printf ("first k = %d, addr=%x\n",  k, &k);
      omp_set_num_threads (4);
    #pragma omp parallel for firstprivate(k)
        for ( i = 0; i <= 10; i++)
        {
          k=k+1;
          printf ("I am thread %d,i=%d, k = %d, addr=%x\n", omp_get_thread_num (),i, k,&k);
        }
          printf ("last k = %d, addr=%x\n",  k, &k);
    
    first k = 100, addr=980c206c
    I am thread 0,i=0, k = 101, addr=980c2028
    I am thread 1,i=3, k = 101, addr=6f9edf8
    I am thread 1,i=4, k = 102, addr=6f9edf8
    I am thread 1,i=5, k = 103, addr=6f9edf8
    I am thread 3,i=9, k = 101, addr=5b9cdf8
    I am thread 3,i=10, k = 102, addr=5b9cdf8
    I am thread 2,i=6, k = 101, addr=659ddf8
    I am thread 2,i=7, k = 102, addr=659ddf8
    I am thread 2,i=8, k = 103, addr=659ddf8
    I am thread 0,i=1, k = 102, addr=980c2028
    I am thread 0,i=2, k = 103, addr=980c2028
    last k = 100, addr=980c206c
    

    可以看出k的值虽然是每个线程私有的,但是每个线程所拥有的k的初值继承了并行区域之外的共享变量k的值

    lastprivate :

    有时候在并行区域内经过计算的私有变量,在退出并行区域后,需要将最后一次循环迭代的值赋给共享变量

      int i ;
      int k =100 ;
      printf ("first k = %d, addr=%x\n",  k, &k);
      omp_set_num_threads (4);
    #pragma omp parallel for firstprivate(k), lastprivate(k)
         for ( i = 0; i <= 10; i++)
         {
           k=k+1;
           printf ("I am thread #%d,i=%d, k = %d, addr=%x\n", omp_get_thread_num (),i, k,&k);
         }
           printf ("last k = %d, addr=%x\n",  k, &k);
    
    first k = 100, addr=367c259c
    I am thread #0,i=0, k = 101, addr=367c2558
    I am thread #3,i=9, k = 101, addr=e6b77df8
    I am thread #3,i=10, k = 102, addr=e6b77df8
    I am thread #2,i=6, k = 101, addr=e7578df8
    I am thread #2,i=7, k = 102, addr=e7578df8
    I am thread #2,i=8, k = 103, addr=e7578df8
    I am thread #1,i=3, k = 101, addr=e7f79df8
    I am thread #1,i=4, k = 102, addr=e7f79df8
    I am thread #1,i=5, k = 103, addr=e7f79df8
    I am thread #0,i=1, k = 102, addr=367c2558
    I am thread #0,i=2, k = 103, addr=367c2558
    last k = 102, addr=367c259c
    

    可以看到最后共享变量k的值是并行区域最后一次循环(i=10)计算后的值。

    threadprivate : / copyin :

    用来指定全局的对象被各个线程各自复制了一个私有的拷贝,即各个线程具有各自私有的全局对象

    /*
     * 实现一个各个线程私有的计数器
    */
    #include "stdio.h"
    #include "omp.h"
    int counter =0 ;
    #pragma omp threadprivate(counter)    //指定全局变量为各个线程的拷贝
    void main()
    {
       int i,j ;
       omp_set_num_threads(4);
      #pragma omp parallel for  
          for (i=0;i<5;i++){
             counter ++ ;
             printf("I am thread %d,i=%d,counter=%d,addr=%x\n",omp_get_thread_num(),i,counter,&counter);
          }
        #pragma omp barrier   //等待前面的循环结束
        printf ("global counter=%d,addr=%x\n",counter,&counter)  ;     //主线程输出全局变量
        #pragma omp parallel for 
          for (j=0;j<4;j++){
             counter ++ ;
             printf("I am thread %d,i=%d,counter=%d,addr=%x\n",omp_get_thread_num(),j,counter,&counter);
         }
       printf("global  counter=%d,addr=%x\n",counter,&counter);
    }
    
    I am thread 0,i=0,counter=1,addr=bc21c75c
    I am thread 0,i=1,counter=2,addr=bc21c75c
    I am thread 2,i=4,counter=1,addr=bb81a6fc
    I am thread 1,i=2,counter=1,addr=bc21b6fc
    I am thread 1,i=3,counter=2,addr=bc21b6fc
    global counter=2,addr=bc21c75c
    I am thread 0,i=0,counter=3,addr=bc21c75c
    I am thread 2,i=2,counter=2,addr=bb81a6fc
    I am thread 3,i=3,counter=1,addr=bae196fc
    I am thread 1,i=1,counter=3,addr=bc21b6fc
    global  counter=3,addr=bc21c75c
    

    从结果很容易看出,不同线程的counter的地址不同,说明每个线程都拷贝了全局变量作为自己的私有的变量。全局变量是主线程0创建的,所以全局变量的值也就是主线程0计算后的值。

    copyin是用来将主线程中被threadprivate声明的变量值拷贝到并行区域的各个线程的threadprivate变量中。对各个线程的threadprivate的值进行初始化。这样说有点抽象,看下面的例子:

    int counter =100 ;
    #pragma omp threadprivate(counter)
    void main()
    {
       int i,j ;
       omp_set_num_threads(4);
       #pragma omp parallel for   
          for (i=0;i<5;i++){
             counter ++ ;
             printf("I am thread %d,i=%d,counter=%d,addr=%x\n",omp_get_thread_num(),i,counter,&counter);
          }
        #pragma omp barrier
        printf ("global counter=%d,addr=%x\n",counter,&counter);
        #pragma omp parallel for copyin(counter)   
          for (j=0;j<4;j++){
             counter ++ ;
             printf("I am thread %d,i=%d,counter=%d,addr=%x\n",omp_get_thread_num(),j,counter,&counter);
         }
       printf("global  counter=%d,addr=%x\n",counter,&counter);
    }
    
    I am thread 0,i=0,counter=101,addr=8735575c
    I am thread 0,i=1,counter=102,addr=8735575c
    I am thread 2,i=4,counter=101,addr=869536fc
    I am thread 1,i=2,counter=101,addr=873546fc
    I am thread 1,i=3,counter=102,addr=873546fc
    global counter=102,addr=8735575c
    I am thread 1,i=1,counter=103,addr=873546fc   
    I am thread 0,i=0,counter=103,addr=8735575c
    I am thread 2,i=2,counter=103,addr=869536fc
    I am thread 3,i=3,counter=103,addr=85f526fc
    global  counter=103,addr=8735575c
    

    第一个循环后,全局变量counter的值为102,第二个循环中,每个线程的counter的初始值都变成了全局变量counter的值102 。

    shared : / default :

    声明一个或者多个变量为多个线程共有的变量,包括主线程 。

      int i ;
      int k =100 ;
      printf ("first k = %d, addr=%x\n",  k, &k);
      omp_set_num_threads (4);
      #pragma omp parallel for shared(k) // 将k变量声明为所有线程共有的变量
        for (i = 0; i <= 10; i++)
        {
          k=k+1 ;
          printf ("I am thread %d,i = %d, k=%d,addr=%x\n", omp_get_thread_num (), i,k,&k);
        }
          printf ("last k = %d, addr=%x\n",  k, &k);
    
    first k = 100, addr=bca2bac
    I am thread 0,i = 0, k=101,addr=bca2bac
    I am thread 2,i = 6, k=102,addr=bca2bac
    I am thread 2,i = 7, k=105,addr=bca2bac
    I am thread 2,i = 8, k=106,addr=bca2bac
    I am thread 3,i = 9, k=101,addr=bca2bac
    I am thread 3,i = 10, k=107,addr=bca2bac
    I am thread 1,i = 3, k=103,addr=bca2bac
    I am thread 1,i = 4, k=108,addr=bca2bac
    I am thread 1,i = 5, k=109,addr=bca2bac
    I am thread 0,i = 1, k=104,addr=bca2bac
    I am thread 0,i = 2, k=110,addr=bca2bac
    last k = 110, addr=bca2bac
    

    其实不加shared(k),效果也是一样的,因为并行执行默认就是共享的。也可以用default (shared)来表明并行块中的变量在不指定的情况下都是shared属性,即共享的 。 效果也是一样的。如下:

      int i ;
      int k =100 ;
      printf ("first k = %d, addr=%x\n",  k, &k);
      omp_set_num_threads (4);
    #pragma omp parallel for default(shared) 
        for (i = 0; i <= 10; i++)
        {
          k=k+1 ;
          printf ("I am thread %d,i = %d, k=%d,addr=%x\n", omp_get_thread_num (), i,k,&k);
        }
          printf ("last k = %d, addr=%x\n",  k, &k);
    

    default 还有另个参数是default(none)表示示必须显式指定所有共享变量的数据属性,否则会报错,除非变量有明确的属性定义(比如循环并行区域的循环迭代变量只能是私有的)
    如下代码会出现错误

      int i ;
      int k =100 ;
      int h=10 ;
      printf ("first k = %d, addr=%x\n",  k, &k);
      omp_set_num_threads (4);
      #pragma omp parallel for private(k)  default(none)   //只指明了k变量的属性,没有指明h变量的属性。程序就会报错,说必须给所有的并行区域中的变量指明属性
        for (i = 0; i <= 10; i++)
        { 
          k=k+1 ;
          h=h+1;
          printf ("I am thread %d,i = %d, k=%d,addr=%x\n", omp_get_thread_num (), i,k,&k);
        }     
      printf ("last k = %d, addr=%x\n",  k, &k);
    
    sharedDemo_01.c: In function 'main':
    sharedDemo_01.c:15: error: 'h' not specified in enclosing parallel
    sharedDemo_01.c:11: error: enclosing parallel
    

    reduction :

    用来对变量指定一个操作符。每个线程都会创建reduction变量的私有拷贝,在并行区域结束处,将使用各个线程的私有拷贝的值通过指定的操作符进行迭代运算,并赋值给原来的变量。

    各个操作符和操作符的初始值如下表:

    操作符 初始值 操作符 初始值
    + 0 - 0
    * 1 & ~0
    | 0 ^ 0
    && 1 || 0

    使用reduction子句可以避免数据竞争,不需要加锁保护

    /*
       加和操作
    */
    #include "omp.h"
    #include "stdio.h"
    int main()
    {
       int i  ;
       int sum=10  ;  //并行区域外的sum值,这个值在并行计算结束后,加到里面
        omp_set_num_threads(4) ;
       #pragma omp parallel for reduction(+:sum)    
      // 每个线程将拷贝sum变量作为自己的的私有变量,初始值为默认的 0 。并且给sum变量指定操作符‘+’ ,每个线程计算后的结果将进行相加
       for (i=0;i<10;i++){
           sum += i ; 
           printf ("I am thread %d,i = %d,sum=%d\n",omp_get_thread_num(),i,sum);
       }
        // 并行计算结束后,计算后的结果再加上并行区之外的sum值100,然后把最后的结果赋值给变量sum .
       printf ("last sum=%d\n",sum);
    }
    
    I am thread 0,i = 0,sum=0     //线程 0 ,进行了本线程3次循环的第一次循环,sum初始值为0,进行了sum += 0 操作后,sum的值变成 0
    I am thread 0,i = 1,sum=1
    I am thread 0,i = 2,sum=3
    I am thread 3,i = 9,sum=9
    I am thread 2,i = 6,sum=6     //线程 2,进行本线程3次循环的第一次循环,sum初始值为0,进行了sum += 6 操作后,sum的值变成 6
    I am thread 2,i = 7,sum=13
    I am thread 2,i = 8,sum=21
    I am thread 1,i = 3,sum=3   //线程1,进行本线程3次循环的第一次循环,sum初始值为0,进行了sum += 3 操作后,sum的值变成 3
    I am thread 1,i = 4,sum=7
    I am thread 1,i = 5,sum=12
    last sum=55
    

    copyprivate :

    用于将线程私有副本变量的值从一个线程广播到执行同一并行区域的其他线程的同一变量。copyprivate只能用于single指令的子句中,在一个single块的结尾处完成广播操作。copyprivate 只能用于private / firstprivate / threadprivate修饰的变量。

    #include "omp.h"
    #include "stdio.h"
    int counter =100 ;
    #pragma omp threadprivate(counter)   //counter变量被threadprivate声明
    void main()
    {
       int i,j ;
       omp_set_num_threads(4);
       #pragma omp parallel 
       {
           int count ;
          #pragma omp single  copyprivate(counter)  //将单线程私有副本变量的值从一个线程广播到并行区域的其他线程的变量
              counter =50 ;
           counter++ ;
           count = counter ;
           printf("I am thread %d , count = %d\n",omp_get_thread_num(),count);
       }      
    } 
    
    I am thread 0 , count = 51
    I am thread 2 , count = 51
    I am thread 1 , count = 51
    I am thread 3 , count = 51
    

    从运行结果看,因为单线程的变量counter的值50,广播给了其他线程 ,所以其他线程从主线程拷贝来的counter变成了50 . 如果没有copyprivate(counter),运行结果应该是如下:并行区域内的线程都是自己私有的counter,值是100 , 只有单线程是50 .

    I am thread 0 , count = 101
    I am thread 1 , count = 101
    I am thread 2 , count = 51
    I am thread 3 , count = 101
    

    openMP中的任务调度

    schedule 是为了更好地分配循环中任务的数量,所以也只能用于循环结构,需要注意的是指定不同的任务分配方式对于程序执行效率会有很大的影响。schedule 总共有 static , dynamic , runtime , guided , auto 五种类型(早期版本的 OpenMP 可能没有 auto )。

    静态调度 schedule(static, [chunk_size]) :

    • parallel for没有带schedule子句时,就是默认用static方式。
    • 假设有n次循环,那么任务被分成了n/chunk_size份(小数要加1),每个线程执行一份任务,一份任务有chunk_size次迭代计算(最后一份任务可能因为循环完成而少于chunk_size次)。
    • 这些任务按照 线程 从小到大依次分配的,比如第一份任务就是交给线程0,第二份就是交给线程1.......。也就是说 thread 0 总是领到第一份任务 。
    • 这是自动给你线程分配的任务 。假如所有线程都分配完毕,循环还没有结束的话,再从线程0开始依次分配
    omp_set_num_threads(5);  //指定5个线程
       #pragma omp parallel for schedule(static,4)   //每个线程分配 4 次循环,直到循环被分配完成
         for (i=0 ; i<10;i++)
            printf ("I am thread %d ,i= %d \n",omp_get_thread_num(),i);
    
    I am thread 2 ,i= 8 
    I am thread 2 ,i= 9 
    I am thread 1 ,i= 4 
    I am thread 1 ,i= 5 
    I am thread 1 ,i= 6 
    I am thread 1 ,i= 7 
    I am thread 0 ,i= 0 
    I am thread 0 ,i= 1 
    I am thread 0 ,i= 2 
    I am thread 0 ,i= 3
    

    从结果,可以很明显的看出,我们虽然指定了5个线程,一共有10次循环,如果按照均分的话,一个线程2次循环就可以,但是我们采用了参数4,系统就会按顺序依次给线程 分配任务,一共有10/4+1=3份任务,依次分配,第一份任务给线程0,循环是4次,(从i=0~i=3),第二份任务给线程1,循环是4次,第三份任务给线程2,因为循环要结束了,所以循环只有2次 。每个线程都依次 分配到了4次(最有一个2次)连续的迭代

    • 如果没有参数[chunk_size],系统就会把循环平均分给每个线程 ,如下:
    int i ;
       omp_set_num_threads(5);
       #pragma omp parallel for schedule(static)
         for (i=0 ; i<10;i++)
            printf ("I am thread %d ,i= %d \n",omp_get_thread_num(),i);
    
    I am thread 0 ,i= 0 
    I am thread 0 ,i= 1 
    I am thread 2 ,i= 4 
    I am thread 2 ,i= 5 
    I am thread 4 ,i= 8 
    I am thread 4 ,i= 9 
    I am thread 3 ,i= 6 
    I am thread 3 ,i= 7 
    I am thread 1 ,i= 2 
    I am thread 1 ,i= 3
    

    动态调度 schedule(dynamic, [chunk_size]) :

    • 任务的分配是动态进行的,每个 thread 按照chunk_size领到任务后执行完成再向系统领新的任务,知道所有的区块都被分派完 。
    • 与静态调度的区别是任务不在是依次分配给线程了,而是线程按照chunk_size去向系统领任务。一个任务被哪个线程领到都是不可预知的。即第一份任务不再是指定交给线程0了,而是可能给了线程1或者线程2
    • 与静态类似,任务被分成了n/chunk_size份(小数要加1),每个线程执行一份任务chunk_size。
    • 没有指定时, 默认 chunk_size=1。
       omp_set_num_threads(3);
      #pragma omp parallel for schedule (dynamic,2)
         for (i =0;i<10;i++)
            printf ("I am thread %d,i=%d\n",omp_get_thread_num(),i);
    
    I am thread 0,i=2
    I am thread 0,i=3
    I am thread 0,i=6
    I am thread 0,i=7
    I am thread 0,i=8
    I am thread 0,i=9
    I am thread 2,i=4
    I am thread 2,i=5
    I am thread 1,i=0
    I am thread 1,i=1
    

    从结果可以看出,第一份任务线程1领取了,循环是(i=0,1)连续的2次 ;第二份任务线程0领取了,循环是(i=2,3)连续的2次 ;第三份任务线程2领取了,循环是(i=4,5)连续的2次 ;第四份任务线程0领取了,循环是(i=6,7)连续的2次 ;第五份任务线程0领取了,循环是(i=8,9)连续的2次 。

    启发式调度 schedule(guided, [chunk_size]) :

    • 这种调度采用指导性的启发式自调度方式,开始时每个线程会分配较大的循环次数,之后分配的越来越小 。 直到降到chunk_size大小(也就是剩余总循环次数小于chunk_size)。
    • 这种调度首先,它和动态调度一样,任务不是依次分配给线程的,也就是说一个任务被分配到哪个线程是不可预知的 。
    • chunk_size是每个任务循环次数的最小值 。
    • 每次分配的任务的循环次数= 剩余总循环次数/ 线程总数(小数加1 )。
    omp_set_num_threads(3);
       #pragma omp parallel for schedule(guided ,2)
          for (i =0;i<10;i++)
             printf("I am thread %d,i=%d\n",omp_get_thread_num(),i);
    
    I am thread 0,i=4
    I am thread 0,i=5
    I am thread 0,i=8
    I am thread 0,i=9
    I am thread 2,i=6
    I am thread 2,i=7
    I am thread 1,i=0
    I am thread 1,i=1
    I am thread 1,i=2
    I am thread 1,i=3
    

    分析结果可以看出: 第一个任务的循环次数 = 10/3+1=4 ,这个任务(i =0,1,2,3)给了线程1 ; 第二个任务的循环次数 = 6/3=2 ,这个任务(i =4,5)给了线程0 ; 第三个任务的循环次数 = 4/3+1=2 ,这个任务(i =6,7)给了线程2 ; 之后的第四个任务因为剩余的循环次数2小于 chunk_size,所以有两次循环给了线程0 。

    runtime调度 schedule(runtime) :

    • 这一种调度不同于前三种,或者说这并不是一种真实的调度 。 分配任务只有执行到这个部分的时候才会决定,分配的具体方法和chunk_size大小会根据环境变量来确定到底是哪种调度类型,最终使用的调度类型仍然是上述三种调度方式的一种 。
    • 在openmp常见环境环境变量中,可以使用setenv命令来设置OMP—_SCHEDULE环境变量 。Windows下可以在系统属性-高级-环境变量中设置 。 (以下引用来自Oracle Solaris Studio 的OpenMP API用户指南 )

    OMP_SCHEDULE
    为指定了 RUNTIME 调度类型的 DO、PARALLEL DO、for、parallel for 指令/pragma 设置调度类型。
    如果未定义,则使用缺省值 STATIC。value为 "type[,chunk]"
    示例(Linux/Unix):setenv OMP_SCHEDULE 'GUIDED,4' // 目的是指定这个为guided调度方式,并且chunk_size = 4

  • 相关阅读:
    keeprunning1的使用说明
    团队冲刺第十五天
    团队冲刺第十四天
    团队第一阶段冲刺评价
    团队第一阶段成果展示
    团队冲刺第十三天
    团队冲刺第十二天
    团队冲刺第十一天
    团队冲刺第十天
    团队冲刺第九天
  • 原文地址:https://www.cnblogs.com/zyuqiang/p/6754245.html
Copyright © 2020-2023  润新知