欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《并行程序设计导论》01openmp

程序员文章站 2022-07-12 19:54:49
...

预备知识

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=0int 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启动的新线程都将终止,程序只用一个线程恢复执行。这个线程打印结果并终止。

相关标签: openmp