OpenMP是共享存储体系结构上的一个并行编程模型。适合于SMP共享内存多处理系统和多核处理器体系结构
由一组编译制导、运行时库函数(Run-Time routines)和环境变量组成。
OpenMP是一种用于共享内存并行系统的多线程程序设计方案,支持的编程语言包括C、C++和Fortran。OpenMP提供了对并行算法的高层抽象描述,特别适合在多核CPU机器上的并行程序设计。编译器根据程序中添加的pragma指令,自动将程序并行处理,使用OpenMP降低了并行编程的难度和复杂度。当编译器不支持OpenMP时,程序会退化成普通(串行)程序。程序中已有的OpenMP指令不会影响程序的正常编译运行
# include "omp.h"
# include "stdio.h"
int main()
{
int id, numb;
omp_set_num_threads(5);
#pragma omp parallel private(id, numb)
{
id = omp_get_thread_num();
numb = omp_get_num_threads();
printf("I am thread %d out of %d\n",id,numb );
}
}
# include "omp.h"
# include "stdio.h"
int main()
{
int x = 2;
#pragma omp parallel num_threads(2) shared(x)
{
if (omp_get_thread_num() == 0) {
x = 5;
}
else {
printf("1: Thread# %d: x = %d\n", omp_get_thread_num(), x);
}
#pragma omp barrier
if (omp_get_thread_num() == 0) {
printf("2: Thread# %d: x = %d\n", omp_get_thread_num(), x);
}
else {
printf("3: Thread# %d: x = %d\n", omp_get_thread_num(), x);
}
}
}
int main()
{
# ifdef _OPENMP
printf("Compiled by an OpenMP-compliant implementation.\n");
# endif
return 0;
}
OpenMP编程模型以线程为基础,通过编译制导指令制导并行化,有三种编程要素可以实现并行化控制,他们分别是编译制导、API函数集和环境变量
OpenMP的并行化是通过使用嵌入到C/C++或
Fortran源代码中的编译制导语句来实现. 通过对串行程序添加制导语句实现并行化。
支持并行区域、工作共享、同步等。
支持数据的共享和私有化。
支持增量并行。
编译制导指令以制导标识符#pragma omp
开始,后边跟具体的功能指令,格式如:#pragma omp 指令[子句[,子句] …]
。
并行域制导
一个并行域就是一个能被多个线程并行执行的
程序段.
#pragma omp parallel [clauses]
{
BLOCK
}
在并行域结尾有一个隐式同步(barrier)。
子句(clause)用来说明并行域的附加信息。
C/C++子句间用空格分开。
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
:用于指定一个或多个变量是线程专用,后面会解释线程专有和私有的区别。private
:指定一个或多个变量在每个线程中都有它自己的私有副本;firstprivate
:指定一个或多个变量在每个线程都有它自己的私有副本,并且私有变量要在进入并行域或任务分担域时,继承主线程中的同名变量的值作为初值;lastprivate
:是用来指定将线程中的一个或多个私有变量的值在并行处理结束后复制到主线程中的同名变量中,负责拷贝的线程是for或sections任务分担中的最后一个线程;reduction
:用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的归约运算,并将结果返回给主线程同名变量;nowait
:指出并发线程可以忽略其他制导指令暗含的路障同步;num_threads
:指定并行域内的线程的数目;schedule
:指定for任务分担中的任务分配调度类型;shared
:指定一个或多个变量为多个线程间的共享变量;ordered
:用来指定for任务分担域内指定代码段需要按照串行循环次序执行;copyprivate
:配合single指令,将指定线程的专有变量广播到并行域内其他线程的同名变量中;copyin
:用来指定一个threadprivate类型的变量需要用主线程同名变量进行初始化;default
:用来指定并行域内的变量的使用方式,缺省是shared。// 计算Pi的值
# include
# include
static long num_steps = 100000; double step;
#define NUM_THREADS 4
void main()
{
int i; double pi, sum[NUM_THREADS], start_time, end_time;
step = 1.0 / (double)num_steps;
omp_set_num_threads(NUM_THREADS);
start_time = omp_get_wtime();
#pragma omp parallel
{ int id; double x;
id = omp_get_thread_num();
for (i = id, sum[id] = 0.0; i < num_steps; i = i + NUM_THREADS) {
x = (i + 0.5) * step; sum[id] += 4.0 / (1.0 + x * x);
}
}
for (i = 0, pi = 0.0; i < NUM_THREADS; i++) pi += sum[i] * step;
end_time = omp_get_wtime();
printf("Pi = % f\n Running time: %f \n", pi, end_time - start_time);
}
omp_in_parallel
: 判断当前是否在并行域中omp_get_thread_num
: 返回线程号omp_set_num_threads
: 设置后续并行域中的线程格式omp_get_num_threads
: 返回当前并行域中的线程数omp_get_max_threads
:获得并行域中可用的最大线程数omp_get_num_procs
: 返回系统中处理器的个数omp_get_dynamic
:判断是否支持动态改变线程的数目omp_set_dynamic
: 启动或者关闭线程数目的动态改变omp_get_nested
: 判断系统是否支持并行嵌套omp_set_nested
: 启动或者关闭并行嵌套omp_init_lock
初始化一个简单锁omp_set_lock
上锁操作omp_unset_lock
解锁操作,要omp_set_lock函数配对使用。omp_destroy_lock
, omp_init_lock函数的配对操作函数,关闭一个锁OMP_SCHEDULE
:用于for循环并行化后的调度,它的值就是循环调度的类型;OMP_NUM_THREADS
:用于设置并行域中的线程数;OMP_DYNAMIC
:通过设定变量值,来确定是否允许动态设定并行域内的线程数;OMP_NESTED
:指出是否可以并行嵌套#include
#include
using namespace std;
void main()
{
#pragma omp parallel
{
cout << "Test" << endl;
}
system("pause");
}
我的电脑的逻辑处理器是16个(打开【任务管理器】,【性能】,【CPU】查看),所以输出16个test。
输出的结果并不是每个Test后换行,原因是每个线程都是独立运行的,在其中一个线程输出字符“Test”之后还没有来得及换行时,另一个线程直接输出了字符“Test”。而且我们可以发现空行数与每行重复输出的Test数量相等。
设置6个进程
#include
#include"omp.h"
using namespace std;
void main()
{
#pragma omp parallel num_threads(6)
{
cout << "Test" << endl;
}
system("pause");
}
for
:用于for循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担,必须由编程人员自己保证每次循环之间无数据相关性。
#include
#include void main() { omp_set_num_threads(3); #pragma omp parallel { printf("The number of threads : %d seen by thread %d\n", omp_get_thread_num(), omp_get_num_threads()); #pragma omp for for (int i = 1; i <= 5; ++i) { printf("No. %d iteration by thread %d\n", i, omp_get_thread_num()); } } } 3.2 parallel for
parallel for
与parallel
的区别:使用parallel制导指令只是产生了并行域,让多个线程分别执行相同的任务,并没有实际的使用价值。parallel for用于生成一个并行域,并将计算任务在多个线程之间分配,从而加快计算运行的速度。可以让系统默认分配线程个数,也可以使用num_threads子句指定线程个数。#include
#include"omp.h" using namespace std; void main() { #pragma omp parallel for num_threads(6) for (int i = 0; i < 12; i++) { printf("OpenMP Test, 线程编号为: %d\n", omp_get_thread_num()); } system("pause"); }
备注:如果for里面比较简单(执行时间短) ,不建议使用多线程并发, 因为 线程间的调度 也会比较耗时,是一个不小的开销。
#include
#include"omp.h"
using namespace std;
void test()
{
for (int i = 0; i < 80000; i++)
{
}
}
void main()
{
float startTime = omp_get_wtime();
//指定2个线程
#pragma omp parallel for num_threads(2)
for (int i = 0; i < 80000; i++)
{
test();
}
float endTime = omp_get_wtime();
printf("指定 2 个线程,执行时间: %f\n", endTime - startTime);
startTime = endTime;
//指定4个线程
#pragma omp parallel for num_threads(4)
for (int i = 0; i < 80000; i++)
{
test();
}
endTime = omp_get_wtime();
printf("指定 4 个线程,执行时间: %f\n", endTime - startTime);
startTime = endTime;
//指定8个线程
#pragma omp parallel for num_threads(8)
for (int i = 0; i < 80000; i++)
{
test();
}
endTime = omp_get_wtime();
printf("指定 8 个线程,执行时间: %f\n", endTime - startTime);
startTime = endTime;
//指定12个线程
#pragma omp parallel for num_threads(12)
for (int i = 0; i < 80000; i++)
{
test();
}
endTime = omp_get_wtime();
printf("指定 12 个线程,执行时间: %f\n", endTime - startTime);
startTime = endTime;
//不使用OpenMP
for (int i = 0; i < 80000; i++)
{
test();
}
endTime = omp_get_wtime();
printf("不使用OpenMP多线程,执行时间: %f\n", endTime - startTime);
startTime = endTime;
system("pause");
}
从如下的执行结果可以看出,线程使用的越多,执行所用的时间越短。
section语句是用在sections语句里用来将sections语句里的代码划分成几个不同的段,每段都并行执行。用法如下:
#pragma omp [parallel] sections [子句]
{
#pragma omp section
{
}
}
#include"omp.h" #include "stdio.h" void main() { #pragma omp parallel sections { #pragma omp section { printf("section 1 ThreadId = % d \n", omp_get_thread_num()); } #pragma omp section { printf("section 2 ThreadId = % d \n", omp_get_thread_num()); } #pragma omp section { printf("section 3 ThreadId = % d \n", omp_get_thread_num()); } #pragma omp section { printf("section 4 ThreadId = % d \n", omp_get_thread_num()); } } }
由执行结果可以看出各个section里的代码都是并行执行的,并且各个section被分配到不同的线程执行。
使用section语句时,需要注意的是这种方式需要保证各个section里的代码执行时间相差不大,否则某个section执行时间比其他section过长就达不到并行执行的效果了。
5.2 示例2
#include"omp.h" #include "stdio.h" void main(int argc, char* argv) { #pragma omp parallel { #pragma omp sections { #pragma omp section { printf("section 1 ThreadId = %d\n", omp_get_thread_num()); } #pragma omp section { printf("section 2 ThreadId = %d\n", omp_get_thread_num()); } #pragma omp section { printf("section 3 ThreadId = %d\n", omp_get_thread_num()); } #pragma omp section { printf("section 4 ThreadId = %d\n", omp_get_thread_num()); } } #pragma omp sections { #pragma omp section { printf("section 5 ThreadId = %d\n", omp_get_thread_num()); } #pragma omp section { printf("section 6 ThreadId = %d\n", omp_get_thread_num()); } #pragma omp section { printf("section 7 ThreadId = %d\n", omp_get_thread_num()); } #pragma omp section { printf("section 8 ThreadId = %d\n", omp_get_thread_num()); } } } }
这种方式和前面那种方式的区别是,两个sections语句是串行执行的,即第二个sections语句里的代码要等第一个sections语句里的代码执行完后才能执行。但是同一个section 内部的顺序可以任意改变。如下图, (1、2、3、4)总是在(5,6,7,8)的前面。
用for语句来分摊是由系统自动进行,只要每次循环间没有时间上的差距,那么分摊是很均匀的,使用section来划分线程是一种手工划分线程的方式,最终并行性的好坏得依赖于程序员。
5.3 示例3
#include
#include int main() { #pragma omp parallel sections { #pragma omp section for (int i = 0; i < 5; ++i) { printf("section i : iteration % d by thread no. % d\n", i, omp_get_thread_num()); } #pragma omp section for (int j = 0; j < 5; ++j) { printf("section j : iteration % d by thread no. % d\n", j, omp_get_thread_num()); } } } 由上图的实验结果可以看出,每个section 模块分配有一个线程,在单个section的模块中,按照次序迭代5次,但是不同的section 模块i和j中的迭代语句可以交叉,即并行运行。说明sections语句里用来将sections语句里的代码划分成几个不同的段,每段都并行执行。
6 . private的作用
// paralle Construct # include "omp.h" # include "stdio.h" int main() { int id, numb; omp_set_num_threads(5); #pragma omp parallel private(id, numb) { id = omp_get_thread_num(); numb = omp_get_num_threads(); printf("I am thread %d out of %d\n",id,numb ); } }
通过子句
num_thread
可以显式控制创建的线程数,如上图所示,创建的线程数为5,通过private
指定id和numb在每个线程中都有它自己的私有副本。同时可以发现每次运行时线程ID出现的次序不同,说明多线程并发运行时,其速度并不是完全一样的而是随机的。
7. barrier 的作用
barrier
:用于并行域内代码的线程同步,线程执行到barrier时要停下等待,直到所有线程都执行到barrier时才继续往下执行#include
#include int main(){ #pragma omp parallel { for (int i = 0; i < 10; i++) { printf("loop i:iteration %d by thread no.%d\n", i, omp_get_thread_num()); } #pragma omp barrier for (int j = 0; j < 10; j++) { printf("loop j:iteration %d by thread no.%d\n", j, omp_get_thread_num()); } } return 0; }
由上图结果可以看出,在一个循环内,每个线程都被迭代10次,这个过程是并行的。但循环之间是按照次序进行的,即loop i 全部执行完后才执行loop j的内容。说明了线程执行到barrier时要停下等待,直到所有线程都执行到barrier时才继续往下执行。8. critical 的作用
critical
:用在一段代码临界区之前,保证每次只有一个OpenMP线程进入。临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待
shared
:指定一个或多个变量为多个线程间的共享变量;#include
#include int main() { int x; x = 0; #pragma omp parallel shared(x) { #pragma omp critical x += 1; } printf("x=%d\n", x); }
其中x被设置为多个线程之间的共享变量,由多个线程分别执行一次迭代,在每次的迭代过程中x的值加1,因此最后的x的值等于线程的个数。9. atomic 的作用
atomic
:用于指定一个数据操作需要原子性地完成,原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束。
shared
:指定一个或多个变量为多个线程间的共享变量#include
#include int main() { int x; x = 0; //omp_set_num_threads(4); #pragma omp parallel shared(x) { #pragma omp atomic x += 1; } printf("x=%d\n", x); } 该结果与上一题中使用critical的结果一样,相当于不同线程间串行运行。其中的critical 和atomic 的区别在于:critical 可以对代码块进行临界区设置,而atomic只能对代码语句进行加持。 原子操作是要独占处理器,其他的线程必须等原子操作完了才可以运行。
10. reduction
模拟计算圆周率的数学公式如下:
π 4 = ∫ 0111 + x 2 d x π 4 = ∫ 0 1 1 1 + x 2 d x 4 π = ∫ 01 1 + x 21 d x π 4 = ∫ 0 1 1 1 + x 2 d x \frac{\pi}{4}=\int_{0}^{1} \frac{1}{1+x^{2}} d x 4π=∫011+x21dx π4=∫0111+x2dx4π=∫011+x21dx4π=∫011+x21dx
把0-1下面积分为n个小矩形,再在每个处理器上处理一部分面积,最后加起来。
#pragma omp parallel for
用在一个for循环的前面,表示下面的一行代码或代码块要分配到多个执行单元中并行计算。
private(local)
默认情况下定义在并行代码之外的变量为各并行的执行单元所共享,使用private限制,表示每个执行单元创建该变量的一个副本
reduction
:用来指定一个或多个变量是私有的,并且在并行处理结束后这些变量要执行指定的归约运算,并将结果返回给主线程同名变量。10.1 parallel for 计算pi
#include
#include static long num_steps = 100000; double step; #define NUM_THREADS 2 void main() { int i; double x, pi, sum = 0.0; step = 1.0 / (double)num_steps; omp_set_num_threads(NUM_THREADS); #pragma omp parallel for reduction(+:sum) private(x) for (i = 1; i <= num_steps; i++) { x = (i - 0.5) * step; sum += 4.0 / (1.0 + x * x); } pi = step * sum; printf("pi=%f\n", pi); }
为该程序分配多个线程,其中指定x为private,即每为每个线程创建一个x的副本。reduction(+:sum)表示并行代码执行完毕后对各个执行单元中的sum进行相加操作,说明每个线程计算一个小长方形的面积,根据积分的原理,最后所有的小长方形的面积之和近似等于圆周率的值。10.2 parallel 计算 pi
#include
#include static long num_steps = 100000; double step; #define NUM_THREADS 2 void main() { int i,id; double x, pi, sum = 0.0; step = 1.0 / (double)num_steps; omp_set_num_threads(NUM_THREADS); #pragma omp parallel private(x,i,id) reduction(+:sum) { id = omp_get_thread_num(); for (i = 1+id; i <= num_steps; i=i+NUM_THREADS) { x = (i - 0.5) * step; sum += 4.0 / (1.0 + x * x); } } pi = step * sum; printf("pi=%f\n", pi); } 通过
for (i = 1+id; i <= num_steps; i=i+NUM_THREADS)
来分配每个处理器计算的矩形id, 然后每个线程并行计算,最后的和累加即可得到圆周率的估计值 pi=3.141593。