《并行程序设计导论》01openmp
预备知识
Openmp提供“基于指令”的共享内存API。这意味着在c和c++中,有一些特殊的预处理器指令pragma。不支持pragma的编译器就会忽略pragma指令提示的那些语句,这样就允许使用pragma的程序在不支持他们的平台上运行。因此,在理论上,如果你仔细编写一个openmp程序,他就能够在任何有c编译器的系统上被编译和运行,无论编译器是否支持openmp。
一个使用openmp的hello world程序
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>
void hello(void);
int main(int argc,char* argv[])
{
int thread_count=strtol(argv[1],NULL,10);//第一个参数从命令行获得线程数,第二个参数没用,第三个表示10进制
# pragma omp parallel num_threads(thread_count);//parallel指令,用来表明之后的结构化代码块
hello();
return 0;
}
void hello(void)
{
int my_rank=omp_get_thread_num();
int thread_count=omp_get_num_threads();
printf("hello from thread %d of %d\n",my_rank,thread_count);
}
用gcc编译这个程序,需要包含-fopenmp选项
gcc -g -Wall -fopenmp -o omp_hello omp_hello.c
运行程序
./omp_hello 4
注意到线程正在竞争访问标准输出,因此不保证输出按照正常序号输出
程序
除了指令集合外,openmp是有一个函数和宏库组成。
在一般的调用线程时,我们必须写很多代码来派生和合并多个线程:需要为每个线程的特殊结构来分别陪存储空间,因此需要for循环来启动每个线程,并使用另一个for循环来终止这些线程。线程被同一进程派生,这些线程共享他们的进程的大部分资源,但每个线程有自己的栈和程序计数器。
openmp总是以##pragma开始(注意,不支持pragma的编译器会自动忽略它,所以openmp程序可以在任何的c编译器运行),最基本的parallel指令可以如下简单的形式:
#pragma omp parallel
运行结构化代码块(要并行的c语句)的线程数将由运行时系统决定。如果没有其他线程启动,典型情况下会将在每个核上运行一个线程。
如果想要在指定线程数,通常会在命令行里指定,在pragma指令增加num_threads子句。在openmp中,子句是用来修改指令的文本。
#pragma omp parallel num_threads(thread_count)
需要注意,程序可以启动的线程数可能会受到系统定义的限制。openmp标准并不保证实际情况下能启动thread_count个线程.
程序运行流程:
在到达parallel之前,程序只使用一个线程。
当程序开始执行,进程开始启动,到达parallel指令时,原来的线程继续执行,另外thread_count-1个线程被启动。
执行并行块的线程集合(原始线程和新的线程)称为线程组,原始的线程称为主线程,额外的线程称为从线程。每个线程都会调用hello函数。
在代码块执行完时,当线程从hello返回时,有一个隐式路障。完成代码快的线程将等待线程组中的所有其他线程完成代码块。
当所有线程都完成代码块,从进程终止,主线程继续执行之后的代码。
因为每个线程有自己的栈,所以执行一个hello函数的线程将在函数中创建自己的私有局部变量。
int omp_get_num_threads(void);//得到线程数
int omp_get_thread_num(void);//得到线程号
错误检查
首先一定要检查命令行参数的存在,如果存在,调用strtol后应该检查值是否是正数。还要检查被parallel指令实际创建的线程数和thread_count是否一样。
第二个潜在风险的来源是编译器。要去诶的那个编译器是否支持openmp。可以检查预处理器宏_OPENMP是否定义。
在头文件处
#ifdef _OPENMP
# include<omp.h>
#endif
在并行代码块处
# ifdef _OPENMP
int my_rank=omp_get_thead_num();
int thread_count=omp_get_num_threads();
# else
int my_rank=0;
int thread_count=1;
# endif
梯形积分法
第一个openmp梯形积分法程序
#include<stdio.h>
#include<stdlib.h>
void Trap(double a,double b,int n,double* global_result_p);
int main()
{
double global_result=0.0;
double a,b;
int n;
int thread_count;
thread_count=strtol(argv[1],NULL,10);
printf("enter a,b, and n\n");
scanf("%lf %lf %d",&a,&b,&n);
#pragma omp parallel num_threads(thread_count)
trap(a,b,n,&global_result);
printf("With n=%d trapezoids, our estimate\n",n);
printf("of the intergral from %f to %f =%.14e\n",a,b,global_result);
return 0;
}
void trap(double a,double b,int n,double* global_result_p)
{
double h,x,my_result;
double local_a,local_b;
int i,local_n;
int my_rank=omp_get_thread_num();
int thread_count=omp_get_num_threads();
h=(b-a)/n;
local_n=n/thread_count;
local_a=a+my_rank*local_n*h;
local_b=local_a+local_n*h;
my_result=(f(local_a)+f(local_b))/2.0;
for(i=1;i<=local_n-1;i++)
{
x=local_a+i*h;
my_result+=f(x);
}
my_result=my_result*h;
/
*使用一个共享变量作为所有线程之和,每个线程可以将它计算的部分结果累加到共享变量中。
然而这可能会导致一个错误的global_result值——如果两个线程试图同时执行这条语句。
除非一个线程在其他线程开始时就完成了计算*global_result_p+=my_result,否则结果都将是不正确的。
这其实是一个竞争条件的例子:多个线程试图访问一个共享资源,并且至少其中一个访问是更新该共享资源,这可能会出现错误。
*/
#pragma omp critical//在openmp中使用critical指令,这条指令告诉编译器需要安排线程对下列的代码块进行互斥访问,即一次只有一个线程能够执行下面的结构化代码
*global_result_p+=my_result;
//引起竞争条件的代码称为临界区。临界区是一个被多个更新共享资源的线程执行的代码,并且共享资源一次只能被一个线程更新
}
解释上面的程序:
在main函数中,第14行前面的代码都是单线程的,它简单获取线程数和输入。在第16行里,parallel指令明确trap函数应该被thread_count个线程执行。在从trap调用返回后,任何被parallel启动的新线程都将终止,程序只用一个线程恢复执行。这个线程打印结果并终止。
上一篇: 《并行程序设计导论》03openmp
下一篇: OpenMP 并行区域之间的工作共享方法