使用OpenMP需要在编译器上打开OpenMP开关,并包含omp.h文件。我使用的是在Windows下的Visual Studio 2015,只需在工程选项中打开OpenMP支持就可以了。按照书上的说法,GCC增加参数-fopenmp就可以了。
OpenMP有两个重要的函数:
omp_get_thread_num()
omp_get_num_threads()
他们的返回值都是无符号整数,第一个用来返回当前执行的线程编号,而第二个返回一共有的线程数量。和C语言的数组下标类似,当前执行的线程编号的值从0开始,最大值总是线程数量减一。至于这两个函数的用途,是进行线程的任务分配。
#pragma omp为OpenMP的预处理器指令,他将告诉编译器的OpenMP的具体行为。
在5.1节中主要涉及到了OpenMP的#pragma omp parallel指令,用来将当前指令并行化。注意这个指令并不包括任何的任务分配,只是简单的将他下面的指令送给不同线程去执行而已。所以,简单的一条#pragma omp parallel指令下面接一个for循环,而for循环的起止点都是常数而不是根据当前线程分配的,只会把这个for循环用线程数量执行一遍,与我们预期的“将内容拆开,分配给不同线程”的想法相悖,无法起到并行化的效果。
书后面提到#pragma omp parallel for就可以自动分配线程任务,但是具体的分配方式还是取决于具体的控制语句。
对于OpenMP指令,还可以跟上一些从句(clause)。比如,在上面的#pragma omp parallel,可以跟上num_threads(thread_count),表示用thread_count的线程数量来运行。除此之外还有一条#pragma omp critical指令,用来在不同线程访问或者更新所有线程共享的资源(比如全局变量),用来防止竞争的情况(race condition)。
书上还说了一些概念性的东西:不同的线程是从进程forked出来的,共享大部分进程的资源(比如stdin,stdout,等),但是不同的线程享有不同的栈(stack)空间和程序计数器(program counter)。当一个线程执行完毕,将返回进程(的主线程)。不同的栈空间意味着局部变量是独立的,尽管可能是使用了同一个函数,但是他们的局部变量在OpenMP创建了子线程后依然是相互独立不相干的。
例题:用OpenMP实现定义法计算曲边梯形(定积分几何意义)的并行算法。
定义法计算定积分包括分割,求和,取极限三个步骤。串行算法比较容易实现,用循环累加分割的小区间的值就可以了。并行的时候,因为各个小区间求和互不相干(没有依赖性(dependence)和相互通信(communication)),因此只需要将不同小区间分配给不同线程就可以了。最后#pragma omp critical指令强调了汇总操作时只能由一个线程更新其共享资源(也相当于锁)。
例子的完整的实现如下:
#include#include /*clock() function*/#include#include /*OpenMP header*/#include
doubleresult_parallel;#define N 400000000
double get_approx_serial(double(*func)(double x), double interval_a, double interval_b, long long intn)
{doubleh;double result = 0;doublex_i;inti;
h= (interval_b - interval_a) /n;for (i = 0; i < n; i++)
{
x_i= interval_a + h*i;
result+= func(x_i)*h;
}returnresult;
}void get_approx_parallel(double(*func)(double x), double interval_a, double interval_b, long long intn)
{doubleh;double result = 0;doublex_i;long long intlocal_i_start, local_i_end;intthread_id, total_threads;inti;/*Get thread num*/total_threads=omp_get_num_threads();
thread_id=omp_get_thread_num();/*Task Arrangment between threads*/local_i_start= (n / total_threads)*thread_id;if (thread_id == total_threads - 1)
local_i_end=n;elselocal_i_end= (n / total_threads)*(thread_id + 1);/*approx Algorithm*/h= (interval_b - interval_a) /n;for (i = local_i_start; i < local_i_end; i++)
{
x_i= interval_a + h*i;
result+= func(x_i)*h;
}#pragma omp criticalresult_parallel+=result;return;
}intmain()
{
clock_t start_clock, end_clock;inttotal_thread_num;/*Get total threads to display*/
#pragma omp paralleltotal_thread_num=omp_get_num_threads();
printf("This computer Has %d Threads in total\n", total_thread_num);/*Do serial algorithm and get the used time.*/start_clock=clock();
printf("The area between y=sin(x) , y=0 , x=0 ,x=pi is %lf\n", get_approx_serial(&sin, 0, 3.141593, N));
end_clock=clock();
printf("Serial total use Time %d ms", end_clock -start_clock);/*Do parallel algorithm and get the used time.*/start_clock=clock();#pragma omp parallelget_approx_parallel(&sin, 0, 3.141593, N);
printf("\nThe area between y=sin(x) , y=0 , x=0 ,x=pi is %lf\n", result_parallel);
end_clock=clock();
printf("Parallel total use Time %d ms", end_clock -start_clock);
system("pause");return 0;
}
critical语句的反汇编情况如下,证明了编译器在解释这条语句时转换成了两个函数调用,来处理线程的竞争冲突。
程序的运行结果如下所示:我的机器是6核12线程的,所以一共有12条线程可以使用,并行算法比串行算法实际加速比(Speedup)$S=T_s/T_p=9912/984=10.07$,效率(efficiency)$E=(1/p)S=0.839$(计算方法和定义见书58页)。根据阿姆达尔定律(Amdahl's law) ,随着数据规模$N$的增大,串行代码占比几乎为零(只有#pragma omp critical子句声明的为串行代码),这个算法可以达到理论上几乎线性的加速比。实际上因为SMT超线程效率有限,加速比为并未达到12。
参考书籍:并行程序设计导论(英文版),机械工业出版社。