OpenMP 共享内存的并行编程框架入门详解

目录
  • 简介
  • 认识 openmp 的简单易用性
    • C 语言实现
    • C++ 实现
    • OpenMP 实现
  • opnemp 基本原理
  • 积分例子
  • 总结

简介

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 在具体的使用场景下的并行。比如我们求函数 x2x^2x2 的积分。

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

如果你不懂上面所谈到的求解方法也没关系,只需要知道我们需要使用 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 的子句和指令的使用方法

以上就是OpenMP 共享内存的并行编程框架入门详解的详细内容,更多关于OpenMP 共享内存并行编程框架的资料请关注我们其它相关文章!

(0)

相关推荐

  • 安装OpenMPI来配合C语言程序进行并行计算

    安装OPENMPI 由于是实验,也不进行多机的配置了,只在虚拟机里安装吧.多个机器的配置可以参考此文 最简单的方法,apt安装 sudo apt-get install libcr-dev mpich2 mpich2-doc 测试 hello.c /* C Example */ #include <mpi.h> #include <stdio.h> int main (int argc, char* argv[]) { int rank, size; MPI_Init (&

  • 详解CLion配置openMP的方法

    使用MinGW64在Clion中配置openMP的开发 安装MinGW64和CLion配置CMakeList.txtCLion 2020.2.3 Build #CL-202.7319.72, built on September 18, 2020 对openMP编译制导的格式问题踩坑 下载 MinGW64 CLion 创建工程,配置CMakeList.txt cmake_minimum_required(VERSION 3.17) project(openMP C) set(CMAKE_C_ST

  • OpenMP深入剖析reduction子句教程

    目录 前言 从并发求和开始 解决求和问题的各种办法 使用数组巧妙解决并发程序当中的数据竞争问题 reduction 子句 深入剖析 reduction 子句 加法+操作符 乘法*操作符 逻辑与&&操作符 或||操作符 MIN 最小值 MAX 最大值 & 按位与 |按位或 ^按位异或 总结 前言 在前面的教程OpenMP入门当中我们简要介绍了 OpenMP 的一些基础的使用方法,在本篇文章当中我们将从一些基础的问题开始,然后仔细介绍在 OpenMP 当中 reduction 子句的各

  • OpenMP 共享内存的并行编程框架入门详解

    目录 简介 认识 openmp 的简单易用性 C 语言实现 C++ 实现 OpenMP 实现 opnemp 基本原理 积分例子 总结 简介 OpenMP 一个非常易用的共享内存的并行编程框架,它提供了一些非常简单易用的API,让编程人员从复杂的并发编程当中释放出来,专注于具体功能的实现.openmp 主要是通过编译指导语句以及他的动态运行时库实现,在本篇文章当中我们主要介绍 openmp 一些入门的简单指令的使用. 认识 openmp 的简单易用性 比如现在我们有一个任务,启动四个线程打印 he

  • Python并行分布式框架Celery详解

    Celery 简介 除了redis,还可以使用另外一个神器---Celery.Celery是一个异步任务的调度工具. Celery 是 Distributed Task Queue,分布式任务队列,分布式决定了可以有多个 worker 的存在,队列表示其是异步操作,即存在一个产生任务提出需求的工头,和一群等着被分配工作的码农. 在 Python 中定义 Celery 的时候,我们要引入 Broker,中文翻译过来就是"中间人"的意思,在这里 Broker 起到一个中间人的角色.在工头提

  • Java进程内缓存框架EhCache详解

    目录 一:目录 二: 简介 2.1.基本介绍 2.2.主要的特性 2.3. 集成 2.4. ehcache 和 redis 比较 三:事例 3.1.在pom.xml中引入依赖 3.2.在src/main/resources/创建一个配置文件 ehcache.xml 3.3.测试类 3.4.缓存配置 一:xml配置方式: 二:编程方式配置 3.5.Ehcache API 四:Spring整合 4.1.pom.xml 引入spring和ehcache 4.2.在src/main/resources添

  • OpenMP中For Construct对dynamic的调度方式详解

    目录 前言 前置知识 dynamic 调度方式分析 实例分析 总结 前言 在本篇文章当中主要给大家介绍 OpenMp for construct 的实现原理,以及与他相关的动态库函数分析,与 for construct 非常相关的是循环的调度方式,在 OpenMP 当中一共有四种调调方式,auto, dynamic, guided, runtime, 在本篇文章当中主要是对 dynamic 的调度方式进行分析. 前置知识 在介绍 for construct 的实现原理之前,我们首先需要了解一下编

  • java property配置文件管理工具框架过程详解

    这篇文章主要介绍了java property配置文件管理工具框架过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 property property 是 java 实现的 property 框架. 特点 优雅地进行属性文件的读取和更新 写入属性文件后属性不乱序 灵活定义编码信息 使用 OO 的方式操作 property 文件 支持多级对象引用 快速开始 环境依赖 Maven 3.x Jdk 1.7+ Maven 引入依赖 <depende

  • C++元编程语言初步入门详解

    目录 模板 泛型初步 函数模板 友元 模板参数 元编程的基本概念 可变参数模板 模板 由于模板元编程需要以面向对象为基础,所以如有疑问之处可以先补充一点C++面向对象的知识: C++面向对象这一篇就够了 泛型初步 由于C++是静态强类型语言,所以变量一经创建,则类型不得更改.如果我们希望创建一种应用广泛地复数类型,那么相应地需要基于int.float.double这些基础类型逐一创建,十分麻烦.泛型编程便是为了简化这一过程而生. 能够容纳不同数据类型作为成员的类被成为模板类,其基本方法为在类声明

  • Linux监控cpu以及内存使用情况之top命令(详解)

    top命令是Linux下常用的性能分析工具,比如cpu.内存的使用,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器. top显示系统当前的进程和其他状况,是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止. 比较准确的说,top命令提供了实时的对系统处理器的状态监视.它将显示系统中CPU最"敏感"的任务列表.该命令可以按CPU使用.内存使用和执行时间对任务进行排序:而且该命令的很多特性都可以通

  • C++ 线程(串行 并行 同步 异步)详解

    C++  线程(串行 并行 同步 异步)详解 看了很多关于这类的文章,一直没有总结.不总结的话就会一直糊里糊涂,以下描述都是自己理解的非官方语言,不一定严谨,可当作参考. 首先,进程可理解成一个可执行文件的执行过程.在ios app上的话我们可以理解为我们的app的.ipa文件执行过程也即app运行过程.杀掉app进程就杀掉了这个app在系统里运行所占的内存. 线程:线程是进程的最小单位.一个进程里至少有一个主线程.就是那个main thread.非常简单的app可能只需要一个主线程即UI线程.

  • Java并发编程-volatile可见性详解

    前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? volatile主要对所修饰的变量提供两个功能 可见性 防止指令重排序 <br>本篇博客主要对volatile可见性进行探讨,以后发表关于指令重排序的博文. 什么是可见性? 把JAVA内存模型(JMM)展示得很详细了,简单概括一下 1.每个Thread有一个属于自己的工作内存(可以理解为每个

  • C语言文件操作的入门详解教程

    一.一些需要掌握的知识点 文件有千千万万,但是在我们的程序设计当中,我们谈的文件一般有两种: 1.程序文件 包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe). 2.数据文件 文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件. 而在本节中,我们主要提到的是数据文件. 1.文件名 我们知道,名字都是用来标识和区别事物的,那么文件名也是这样,是区别各个文件的标识. 一个文件

随机推荐