相关概念
并行计算
并行计算(Parallel Computing)是指同时使用多种计算资源解决计算问题的过程,是提高计算机系统计算速度和处理能力的一种有效手段。它具有强大的数值计算和数据处理能力,能满足绝大多数科技人员的使用需求。
超线程
超线程技术把多线程处理器内部的两个逻辑内核模拟成两个物理芯片,让单个处理器就能使用线程级的并行计算,进而兼容多线程操作系统和软件。超线程技术充分利用空闲CPU资源,在相同时间内完成更多工作。
虽然采用超线程技术能够同时执行两个线程,当两个线程同时需要某个资源时,其中一个线程必须让出资源暂时挂起,直到这些资源空闲以后才能继续。因此,超线程的性能并不等于两个CPU的性能。而且,超线程技术的CPU需要芯片组、操作系统和应用软件的支持,才能比较理想地发挥该项技术的优势。
MPI与OpenMP
MPI:
MPI主要针对粗粒度级别的并行,主要应用在分布是计算机上,即将任务分配给集群中所有计算机上。
OpenMP:
OpenMP主要针对细粒度的循环进行并行,即在循环中将每次循环分配给不同的线程去执行,主要应用于一台独立的服务器或计算机上。由于使用线程间共享内存的方式协调并行计算,它在多核/多CPU结构上的效率很高、内存开销小、编程语句简洁直观,因此编程容易、编译器实现也容易。
OpenMP的概念
OpenMP是由主要的计算机硬件和软件厂商共同制定的一种面向共享内存的多CPU多线程并行编程接口。支持的编程语言包括C、C++、Fortran。
OpenMP特点
编程模型 | OpenMP规范的核心是并行区域和并行共享结构。编程人员通过并行共享指令实现程序结构块的并行化和向量化。 |
---|---|
执行模式 | OpenMP对部分循环可采用指令simd显示地向量化。在并行区域采用的是线程的派生和缩并模式。 |
数据环境 | OpenMP规定,在并行区域内,各个子线程拥有各自的私有变量,其他线程不能访问。全部线程均可对共享变量进行读写操作。 |
线程同步 | OpenMP主要利用共享结构后的隐式同步来避免数据竞争,利用指令flush等显示同步来维护共享数据的一致性,利用taskwait、taskgroup实现任务的同步完成。 |
并行计算 | 采用指令task、taskwait、taskgroup、taskloop等实现非规则循环和递归等的并行计算。 |
异构设备 | 利用指令target、task、taskwait等实现异构计算。 |
OpenMP编程——预备知识
OpenMP由编译指导语句、库函数和环境变量三部分组成。其指导思想是将工作划分为多个子任务分配给多个线程,从而实现多核并行处理单一的地址空间。
编译指导语句的格式为:
#pragma omp <directive> [clause[[,]clause]…]
diirective部分是编译指导语句的主要指令,用来指导多个CPU共享任务或指导多个CPU同步;
clause部分是可选的句子,它给出了相应的指令参数,可以影响到编译指令指导语句的具体执行;
常用指令如下:
指令 | 说明 |
---|---|
parallel | 用在一个结构块之前,表示这段代码将被多个线程并行执行; |
for | 用于for循环语句之前,表示将循环计算任务分配到多个线程中并行执行,以实现任务分担,但必须保证每次循环之间无数据相关性; |
sections | 用在要被并行执行对的代码段之前,用于实现多个结构块语句的任务分担,可并行执行的代码段各自用section指令标出(注意区分sections和section); |
critical | 用在一段代码临界区之前,保证每次只有一个OpenMP线程进入; |
single | 用在并行域内,表示一段只被单个线程执行的代码; |
flush | 保证各个OpenMP线程的数据影像的一致性; |
barrier | 用于并行域内代码的线程同步,线程执行到barrier时要停下等待,直到所有线程都执行到barrier时才继续往下执行; |
private | 指定一个或多个变量在每个线程中都有它自己的私有副本; |
shared | 指定一个或多个变量为多个线程间的共享变量; |
default | 用来指定并行域内的变量的使用方式,缺省是shared; |
firstprivate | 指定一个或多个变量在每个线程都有它自己的私有副本,并且私有变量要在进入并行域或任务分担域时,继承主线程中的同名变量的值作为初值; |
lastprivate | 是用来指定将线程中的一个或多个私有变量的值在并行处理结束后复制到主线程中的同名变量中,负责拷贝的线程是for或sections任务分担中的最后一个线程; |
copyin | 用来指定一个threadprivate类型的变量,需要用主线程同名变量进行初始化; |
OpenMP编程——头文件
在Linux系统中使用C/C++编译器时,要通过以下方式包含OpenMP头文件:
#include <omp.h>
该头文件是一个调用库中多种函数的应用编程接口。通过这个文件,编译器才能自动链接正确的库。
常用的OpenMP库函数如下:
指令 | 说明 |
---|---|
omp_in_parallel | 判断当前是否在并行域中; |
omp_set_num_threads | 设置后续并行域中的线程数量; |
omp_get_num_procs | 返回计算系统中处理器的个数; |
omp_get_num_threads | 返回当前并行域中的线程数; |
omp_get_thread_num | 返回当前的线程号; |
omp_get_max_threads | 返回当前并行域中可用的最大线程数; |
omp_get_dynamic | 判断是否支持动态改变线程数量; |
omp_set_dynamic | 启用或关闭线程数量的动态改变; |
omp_init_lock | 初始化一个简单锁; |
omp_set_lock | 给一个简单锁上锁; |
omp_unset_lock | 给一个简单锁解锁; |
omp_destroy_lock | 关闭一个锁并释放内存; |
omp_get_wtime | 相对于某个任意参考时刻而言已经经历的时间 |
变量作用域
变量作用域﹣子句 private
private 子句的语法格式: private (变量列表)
private 子句将一个或多个变量声明为线程的私有变量。每个线程都有它自己的变量私有副本,其他线程无法访问。即使在并行区域外有同名的共享变量,共享变量在并行区域内不起任何作用,并且并行区域内不会操作到外面的共享变量。
变量作用域﹣子句 shared
shared 子句的语法格式: shared (变量列表)
shared 子句将变量列表中一个或多个变量声明为线程组中子线程共享的变量。所谓变量共享,是指在一个并行区域的线程组内,所有线程只拥有该变量的一个内存地址,所有线程对共享变量的访问即是对同一地址的访问。
变量作用域﹣子句 default
default 子句的语法格式:default ( shared | none )
default 子句用来控制并行区域内变量的共享属性,其取值有 shared 和 none 两个。指定为 shared 表示在没有显式指定访问权限时,传入并行区域内的变量访问权限为 shared ;指定为 none 意味着必须显式地为这些变量指定访问权限。
在某些情况下, OpenMP 默认变量访问权限会导致一些问题,如需要 private 访问权限的数组被默认成 shared 了。故建议显式地使用 default ( none )来去掉变量的默认访问权限。
变量作用域﹣规约操作 reduction
reduction 子句的语法格式 :reduction (运算符:变量列表)
reduction 子句可以对前后有依赖的循环进行规约操作的并行化。每个线程将创建参数的一个副本,在运算结束时,将各线程的副本进行指定的操作,操作的结果赋值给原始的参数。出现在变量列表中的变量是一个标量,其变量属性是私有变量,但它们不能同时出现在所在并行区域的 private 子句中。
reduction 支持的操作符是有限的,支持+ - * / += -= *= /= |&^,且不能是 C ++重载后的运算符,具体可参见0penMP规范。
程序示例
源代码:
# include <stdio.h>
# include <omp.h>
int main (){
int tid, mcpu;
tid = omp_get_thread_num(); //返回线程号
mcpu = omp_get_num_threads(); //返回当前并行区域中的线程数
printf (" hello from thread %d in %d CPUs \n ", tid, mcpu);
printf ("------ before parallel\n");
printf ("\n ");
printf ("------ during parallel\n ");
// num_threads 子句指定线程个数为3, private 指定变量在每个线程中都有自己的私有副本
# pragma omp parallel num_threads(3) private(tid, mcpu)
{
tid = omp_get_thread_num();
mcpu = omp_get_num_threads();
printf (" hello from thread %d in %d CPUs\n ", tid, mcpu);
}
printf ("\n");
printf ("------ after parallel\n");
printf ("hello from thread %d in %d CPUs\n ", tid, mcpu);
return 0;
}
编译:
gcc -fopenmp omp.c
运行:
./a.out
运行结果:
hello from thread 0 in 1 CPUs
------ before parallel
------ during parallel
hello from thread 0 in 3 CPUs
hello from thread 2 in 3 CPUs
hello from thread 1 in 3 CPUs
------ after parallel
hello from thread 0 in 1 CPUs