当前位置:   article > 正文

并行编程OpenMP基础及简单示例

openmp

1. OpenMP基本概念

OpenMP是一种用于共享内存并行系统的多线程程序设计方案,支持的编程语言包括C、C++和Fortran。OpenMP提供了对并行算法的高层抽象描述,特别适合在多核CPU机器上的并行程序设计。编译器根据程序中添加的pragma指令,自动将程序并行处理,使用OpenMP降低了并行编程的难度和复杂度。当编译器不支持OpenMP时,程序会退化成普通(串行)程序。程序中已有的OpenMP指令不会影响程序的正常编译运行。

OpenMP通过编译指导命令来并行化,什么是编译指导命令?简单来说就是我们平常写的#开头的语句,通过程序中插入的这些编译指导命令,计算机就会完成并行计算的工作。在C/C++程序中,OpenMP的所有的编译指导命令都是以#pragma omp开始的,后面跟具体的功能指导命令,命令形式如下:

#pragma omp 指令 子句,子句,子句……
  • 1

指令可以单独出现,子句必须出现在指令之后。现在如果看不懂也没关系,下面将开始用具体的例子来说明。

2. OpenMP执行模式

OpenMP采用fork-join的执行模式。开始的时候只存在一个主线程,当需要进行并行计算的时候,派生出若干个分支线程来执行并行任务。当并行代码执行完成之后,分支线程会合,并把控制流程交给单独的主线程。

一个典型的fork-join执行模型的示意图如下:

OpenMP编程模型以线程为基础,通过编译制导指令制导并行化,有三种编程要素可以实现并行化控制,他们分别是编译制导、API函数集和环境变量。

3. 编译制导

编译制导指令以#pragma omp 开始,后边跟具体的功能指令,格式如:#pragma omp 指令[子句[,子句] …]。常用的功能指令如下:

  • parallel:用在一个结构块之前,表示这段代码将被多个线程并行执行
  • for:用于for循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担,必须由编程人员自己保证每次循环之间无数据相关性;
  • parallel for:parallel和for指令的结合,也是用在for循环语句之前,表示for循环体的代码将被多个线程并行执行,它同时具有并行域的产生和任务分担两个功能;
  • sections:用在可被并行执行的代码段之前,用于实现多个结构块语句的任务分担,可并行执行的代码段各自用section指令标出(注意区分sections和section);
    parallel sections:parallel和sections两个语句的结合,类似于parallel for;
  • single:用在并行域内,表示一段只被单个线程执行的代码;
  • critical:用在一段代码临界区之前,保证每次只有一个OpenMP线程进入;
  • flush:保证各个OpenMP线程的数据影像的一致性;
  • barrier:用于并行域内代码的线程同步,线程执行到barrier时要停下等待,直到所有线程都执行到barrier时才继续往下执行;
  • atomic:用于指定一个数据操作需要原子性地完成;
  • master:用于指定一段代码由主线程执行;
  • threadprivate:用于指定一个或多个变量是线程专用,后面会解释线程专有和私有的区别。
  • ordered, 用亍指定并行区域的循环按顺序执行
    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:是用来指定将线程中的一个或多个私有变量的值在并行处理结束后复制到主线程中的同名变量中,负责拷贝的线程是for或sections任务分担中的最后一个线程;
  • reduction:用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的归约运算,并将结果返回给主线程同名变量
  • nowait:指出并发线程可以忽略其他制导指令暗含的路障同步;
  • num_threads:指定并行域内的线程的数目;
  • schedule:指定for任务分担中的任务分配调度类型;
  • shared:指定一个或多个变量为多个线程间的共享变量;
  • ordered:用来指定for任务分担域内指定代码段需要按照串行循环次序执行;
  • copyprivate:配合single指令,将指定线程的专有变量广播到并行域内其他线程的同名变量中;
  • copyin:用来指定一个threadprivate类型的变量需要用主线程同名变量进行初始化;
  • default:用来指定并行域内的变量的使用方式,缺省是shared。

4. API函数

除上述编译制导指令之外,OpenMP还提供了一组API函数用于控制并发线程的某些行为,下面是一些常用的OpenMP API函数以及说明:

5. 环境变量

OpenMP中定义一些环境变量,可以通过这些环境变量控制OpenMP程序的行为,常用的环境变量:

  • OMP_SCHEDULE:用于for循环并行化后的调度,它的值就是循环调度的类型; **
    OMP_NUM_THREADS**:用于设置并行域中的线程数; **
    OMP_DYNAMIC**:通过设定变量值,来确定是否允许动态设定并行域内的线程数; **
    OMP_NESTED**:指出是否可以并行嵌套。

6. 简单的语句——开始并行!

前文讲到了openMP的语句方式,现在先来解锁一个最为简单也最为频繁的指令 parallel

#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
#pragma omp parallel
	{
		cout << "Hello, world!" << endl;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

执行结果:

parallel制导命令表示接下来由花括号括起来的区域将创建多个线程并行执行。可以用num_threads子句来控制线程的个数,如下:

#include<omp.h>
#include<iostream>
using namespace std;
int main()
{	
#pragma omp parallel num_threads(5)
	{
		
		cout << "Hello, world!" << endl;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

还可以用一个函数:

#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
	omp_set_num_threads(2);
#pragma omp parallel
	{
		
		cout << "Hello, world!" << endl;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

好了,现在你已经懂了如何让特定的程序区域并行起来,接下来将解锁一个常用的制导命令 for,直接将你的for循环体提升n倍!在并行域里面用以下命令,在这条语句之后的一个for循环语句中每一个要循环的任务将被分配给不同的线程去执行。

例如:

#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
	omp_set_num_threads(2);
#pragma omp parallel
	{
#pragma omp for
		for(int i=0;i<4;i++)
		 cout << omp_get_thread_num() << endl;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

执行结果:

也可以直接把for写在parallel后面

注意,关于for循环有几个大坑需要注意,在这篇博客当中有提到:https://blog.csdn.net/qq_34488063/article/details/53177078

所以,用for循环最好采用for(int i=0;i<N;i++)这种形式就好了,别整太麻烦。

上面的两个制导命令parallel和for已经能够对科学计算程序性能有了足够大的改善,相信大家已经很好地掌握了制导命令的使用方式,接下是关于for循环的调度模式的一些内容。

7. for循环语句的分配模式

在以上的任务中,各个线程自动分配到要执行的任务标号,没有对任务做一些进一步的调度,接下来介绍的字句将会对for循环任务的调度做更细致一些的规定。

用schedule子句进行for循环任务调度的管理

schedule子句形式

type参数有四种:1.static, 2.dynamic, 3.guided, 4.runtime

size参数是整形数据:表示循环迭代次数划分的单位。

1.static参数

静态调度,不用size参数时分配给每个程序的都是n/t次连续迭代,n为迭代次数,t为并行的线程数目。

#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
	omp_set_num_threads(2);
#pragma omp parallel for schedule(static)
		for(int i=0;i<8;i++)
		 cout << omp_get_thread_num() << endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行结果

使用size参数,表示每次分配给线程size次的连续迭代。

2.dynamic参数

动态调度模式是先到先得的方式进行任务分配,不用size参数的时候,先把任务干完的线程先取下一个任务,以此类推,而不是一开始就分配固定的任务数。使用size参数的时候,分配的任务以size为单位,一次性分配size个。虽然很智能,在任务难度不均衡的时候适合用dynamic,否则会引起过多的任务动态申请的开销。

3.guided参数

刚开始每个线程会分配到比较大的迭代块,后来分配到的迭代块逐渐递减,没有指定size就会降到1,否则降到size。

4.runtime

基本不会用到,需要了解的可以自行了解。

8. sections制导指令

用sections把不同的区域交给不同的线程去执行

用法:

#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
	omp_set_num_threads(3);
#pragma omp parallel sections
	{
#pragma omp section
		{
			cout <<omp_get_thread_num();
		}
#pragma omp section
		{
			cout << omp_get_thread_num();
		}
#pragma omp section
		{
			cout << omp_get_thread_num();
		}
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

可以看出这时候三个线程分为三部分并发执行每一个section区域。

9. single制导指令

single制导指令所包含的代码段只有一个线程执行,别的线程跳过该代码,如果没有nowait子句,那么其他线程将会在single制导指令结束的隐式同步点等待。有nowait子句其他线程将跳过等待往下执行。

int main()
{
	omp_set_num_threads(4);
#pragma omp parallel
	{
#pragma omp single
		{
			cout << "single thread=" << omp_get_thread_num()<<endl;
		}
		cout << omp_get_thread_num() << endl;
	}
		
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

执行结果

可以看出,在隐式同步点已经同步。这个代码和运行结果可能不太明显,大家懂这个意思就好了。single由一个线程进行执行,然后其它线程等它执行完然后汇合一起往下执行。

添加上nowait子句就不会在隐式点同步了。

想要了解更详细的内容可以关注:https://github.com/lovelyyoshino/Chinese_Notes/blob/main/Automatic%20driving/OpenMP_simple_Program.pdf
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/265586
推荐阅读
相关标签
  

闽ICP备14008679号