OpenMP 入门

简介

OpenMP 一个非常易用的共享内存的并行编程框架,它提供了一些非常简单易用的API,让编程人员从复杂的并发编程当中释放出来,专注于具体功能的实现。openmp 主要是通过编译指导语句以及他的动态运行时库实现,在本篇文章当中我们主要介绍 openmp 一些入门的简单指令的使用。

认识 openmp 的简单易用性

比如现在我们有一个任务,启动四个线程打印 hello world,我们看看下面 C 使用 pthread 的实现以及 C++ 使用标准库的实现,并对比他们和 openmp 的实现复杂性。

C 语言实现


#include <stdio.h>
#include <pthread.h> void* func(void* args) {
printf("hello world from tid = %ld\n", pthread_self());
return NULL;
} int main() {
pthread_t threads[4];
for(int i = 0; i < 4; i++) {
pthread_create(&threads[i], NULL, func, NULL);
}
for(int i = 0; i < 4; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}

上面文件编译命令:gcc 文件名 -lpthread

C++ 实现

#include <thread>
#include <iostream> void* func() { printf("hello world from %ld\n", std::this_thread::get_id());
return 0;
} int main() { std::thread threads[4];
for(auto &t : threads) {
t = std::thread(func);
}
for(auto &t : threads) {
t.join();
} return EXIT_SUCCESS;
}

上面文件编译命令:g++ 文件名 lpthread

OpenMP 实现

#include <stdio.h>
#include <omp.h> int main() { // #pragma 表示这是编译指导语句 表示编译器需要对下面的并行域进行特殊处理 omp parallel 表示下面的代码区域 {} 是一个并行域 num_threads(4) 表示一共有 4 个线程执行 {} 内的代码 因此实现的效果和上面的效果是一致的
#pragma omp parallel num_threads(4)
{
printf("hello world from tid = %d\n", omp_get_thread_num()); // omp_get_thread_num 表示得到线程的线程 id
}
return 0;
}

上面文件编译命令:gcc 文件名 -fopenmp ,如果你使用了 openmp 的编译指导语句的话需要在编译选项上加上 -fopenmp

从上面的代码来看,确实 openmp 写并发程序的复杂度确实比 pthreadC++ 低。openmp 相比起其他构建并行程序的方式来说,使用 openmp 你可以更加关注具体的业务实现,而不用太关心并发程序背后的启动与结束的过程,OenpMP 会帮我们实现很多细节,让程序的执行符合我们的直觉。

opnemp 基本原理

在上文当中我们写了一个非常简单的 openmp 程序,使用 4 个不同的线程分别打印 hello world 。我们仔细分析一下这个程序的执行流程:

在 openmp 的程序当中,你可以将程序用一个个的并行域分开,在并行域(parallel region)中,程序是有并发的,但是在并行域之外是没有并发的,只有主线程(master)在执行,整个过程如下图所示:

现在我们用一个程序去验证上面的过程:

#include <stdio.h>
#include <omp.h>
#include <unistd.h> int main() { #pragma omp parallel num_threads(4)
{
printf("parallel region 1 thread id = %d\n", omp_get_thread_num());
sleep(1);
}
printf("after parallel region 1 thread id = %d\n", omp_get_thread_num()); #pragma omp parallel num_threads(4)
{
printf("parallel region 2 thread id = %d\n", omp_get_thread_num());
sleep(1);
}
printf("after parallel region 2 thread id = %d\n", omp_get_thread_num()); #pragma omp parallel num_threads(4)
{
printf("parallel region 3 thread id = %d\n", omp_get_thread_num());
sleep(1);
} printf("after parallel region 3 thread id = %d\n", omp_get_thread_num());
return 0;
}

程序执行之后的一种输出(还有很多其他的输出形式,因为是多线程程序,线程的输出是不确定的)如下所示:

parallel region 1 thread id = 0
parallel region 1 thread id = 3
parallel region 1 thread id = 1
parallel region 1 thread id = 2
after parallel region 1 thread id = 0
parallel region 2 thread id = 0
parallel region 2 thread id = 2
parallel region 2 thread id = 3
parallel region 2 thread id = 1
after parallel region 2 thread id = 0
parallel region 3 thread id = 0
parallel region 3 thread id = 1
parallel region 3 thread id = 3
parallel region 3 thread id = 2
after parallel region 3 thread id = 0

从上面的输出我们可以了解到,id = 0 的线程就是主线程,在并行域内部程序的输出是没有顺序的,但是在并行域的外部是有序的,在并行域的开始部分程序会进行并发操作,但是在并行域的最后会有一个隐藏的同步点,等待所有线程到达这个同步点之后程序才会继续执行,现在再看上文当中 openmp 的执行流图的话就很清晰易懂了。

积分例子

现在我们使用一个简单的函数积分的例子去具体了解 openmp 在具体的使用场景下的并行。比如我们求函数 \(x^2\) 的积分。

\[\int_0^{x} x^2 = \frac{1}{3}x^3dx + C
\]

比如我们现在需要 x = 10 时,\(x^2\) 的积分结果。我们在程序里面使用微元法去计算函数的微分结果,而不是直接使用公式进行计算,微元法对应的计算方式如下所示:

\[\int_0^{10} x^2\mathrm{d}x =\sum_{ i= 0}^{1000000}(i * 0.00001) ^2 * 0.00001
\]

微元法的本质就是将曲线下方的面积分割成一个一个的非常小的长方形,然后将所有的长方形的面积累加起来,这样得到最终的结果。

如果你不懂上面所谈到的求解方法也没关系,只需要知道我们需要使用 openmp 去计算一个计算量比较大的任务即可。根据上面微元法的公式我们有一个非常大的求和公式,如果是在单线程的情况下我们使用一个循环就可以了,但是现在我们有多个线程,那么我们可以让每个线程求某一个区间的和,最后将各个区间的和加起来得到最终的结果,这就是在并发场景下的实现思路。

openmp 具体的实现代码如下所示:


#include <stdio.h>
#include <omp.h>
#include <math.h> /// @brief 计算 x^2 一部分的面积
/// @param start 线程开始计算的位置
/// @param end 线程结束计算的位置
/// @param delta 长方形的边长
/// @return 计算出来的面积
double x_square_partial_integral(double start, double end, double delta) { double s = 0;
for(double i = start; i < end; i += delta) {
s += pow(i, 2) * delta;
}
return s;
} int main() { int s = 0;
int e = 10;
double sum = 0;
#pragma omp parallel num_threads(32) reduction(+:sum)
{
// 根据线程号进行计算区间的分配
// omp_get_thread_num() 返回的线程 id 从 0 开始计数 :0, 1, 2, 3, 4, ..., 31
double start = (double)(e - s) / 32 * omp_get_thread_num();
double end = (double)(e - s) / 32 * (omp_get_thread_num() + 1);
sum = x_square_partial_integral(start, end, 0.0000001);
}
printf("sum = %lf\n", sum);
return 0;
}

在上面的代码当中 #pragma omp parallel num_threads(4) 表示启动 4 个线程执行 {} 中的代码,reduction(+:sum) 表示需要对 sum 这个变量进行一个规约操作,当 openmp 中的线程遇到 reduction 子句的时候首先会拷贝一份 sum 作为本地变量,然后在并行域当中使用的就是每一个线程的本地变量,因为有 reduction 的规约操作,因此在每个线程计算完成之后还需要将每个线程本地计算出来的值对操作符 + 进行规约操作,也就是将每个线程计算得到的结果求和,最终将得到的结果赋值给我们在 main 函数当中定义的变量 sum 。最终我们打印的变量 sum 就是各个线程求和之后的结果。上面的代码执行过程大致如下图所示:

注意事项:你在编译上述程序的时候需要加上编译选项 -fopenmp 启动openmp 编译选项和 -lm 链接数学库。

上面程序的执行结果如下所示:

总结

在本篇文章当中主要给大家介绍了 OpenMP 的基本使用和程序执行的基本原理,在后续的文章当中我们将仔细介绍各种 OpenMP 的子句和指令的使用方法,希望大家有所收获!


更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

OpenMP 入门的更多相关文章

  1. OpenMP入门教程(三)

    承接前面两篇,这里直接逐一介绍和使用有关OpenMP的指令和函数 Directives 1.for 作用:for指令指定紧随其后的程序的循环的迭代必须由团队并行执行,只是假设已经建立了并行区域,否则它 ...

  2. OpenMP入门

    OpenMP入门 前情提要:并行(parallel):需要多个运算核心同时完成 其中有多处理器和单处理器多核两种实现方式,其中差异如下: 同一芯片上的多核通信速度更快 同一芯片上的多核能耗更低 Ope ...

  3. OpenMP 入门教程

    前两天(其实是几个月以前了)看到了代码中有 #pragma omp parallel for 一段,感觉好像是 OpenMP,以前看到并行化的东西都是直接躲开,既然躲不开了,不妨研究一下: OpenM ...

  4. 并行计算之OpenMP入门简介

    在上一篇文章中介绍了并行计算的基础概念,也顺便介绍了OpenMP. OpenMp提供了对于并行描述的高层抽象,降低了并行编程的难度和复杂度,这样程序员可以把更多的精力投入到并行算法本身,而非其具体实现 ...

  5. openmp入门总结

    Ref: https://wdxtub.com/2016/03/20/openmp-guide/ 简介 这门课作为 ECE 中少有的跟计算机科学相关的课,自然是必上不可.不过无论是 OpenMP 还是 ...

  6. [转]OpenMP 入门指南

    简介 这门课作为 ECE 中少有的跟计算机科学相关的课,自然是必上不可.不过无论是 OpenMP 还是 CUDA,对于平时极少接触并行编程的我来说,都是十分吃力的,第一次作业的 OpenMP 编程已经 ...

  7. OpenMP入门教程(二)

    OpenMP API概述 OpenMP由三部分组成: 编译指令(19) 运行时库程序(32) 环境变量(9) 后来的API包含同样的三个组件,只是三者的数量都有所增加. 编译器指令 OpenMP编译器 ...

  8. OpenMP入门教程(一)

    什么是OpenMP Open Multi-Processing的缩写,是一个应用程序接口(API),可用于显式指导多线程.共享内存的并行性. 在项目程序已经完成好的情况下不需要大幅度的修改源代码,只需 ...

  9. openMP多线程编程

    OpenMP(Open Muti-Processing) OpenMP缺点: 1:作为高层抽象,OpenMp并不适合需要复杂的线程间同步和互斥的场合: 2:另一个缺点是不能在非共享内存系统(如计算机集 ...

随机推荐

  1. error setting certificate verify locations

    描述 在使用 git clone 克隆 GitHub 或者 Gitee 上的项目时,报如下错误: error setting certificate verify locations: CAfile: ...

  2. DOM及DOM相关操作

    DOM 概述: DOM 全称(document object model)文档对象模型(文档指定为对应html文档),对应的DOM就是操作HTML文档的(增删改查) DOM结构 document 文档 ...

  3. ASP.NET Core自定义中间件的方式

    ASP.NET Core应用本质上,其实就是由若干个中间件构建成的请求处理管道.管道相当于一个故事的框架,而中间件就相当于故事中的某些情节.同一个故事框架采用不同的情节拼凑,最终会体现出不同风格的故事 ...

  4. Linux—进程管理

    Linux 进程管理 1.进程管理介绍 1.1 什么是进程? 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础. 简而言之 ...

  5. 【python】一些python用法规律笔记

    作为本科用了多年MATLAB的工科生,学起来python有些似曾相识但也有些不习惯的地方. 在这里总结一下,慢慢整理,希望能巩固python语法 一.前闭后开 这个是和MATLAB很大不同.不论是ra ...

  6. 【UML分析、建模与设计】我在工作时遇到UML

    一.前言 UML分析.建模与设计 来自现实世界中的概念的抽象描述方法(摘取自<UML面向对象分析.建模与设计(第2版)>) 就我对UML分析与建模技术的认知,最早可追溯至2019年时的学习 ...

  7. 【读书笔记】C#高级编程 第十一章 LINQ

    (一)LINQ概述 语言集成查询(Language Integrated Query,LINQ)在C#编程语言中继承了查询语法,可以用相同的语法访问不同的数据源. 1.LINQ查询 var query ...

  8. [Python]-字典-实践经验总结

    字典是Python中常用的一个数据类型. 与列表有相似的用法,表现在列表的下标和字典的键值可以通过相似的方式读取数据: list_name[0] = value dict_name['key'] = ...

  9. Vue3 封装 Element Plus Menu 无限级菜单组件

    本文分别使用 SFC(模板方式)和 tsx 方式对 Element Plus el-menu 组件进行二次封装,实现配置化的菜单,有了配置化的菜单,后续便可以根据路由动态渲染菜单. 1 数据结构定义 ...

  10. 输入法词库解析(六)QQ 拼音分类词库.qpyd

    详细代码:https://github.com/cxcn/dtool 前言 .qpyd 是 QQ 拼音输入法 6.0 以下版本所用的词库格式,可以在 http://cdict.qq.pinyin.cn ...