乐趣区

关于并发:OpenMP-入门

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-LeHu…

关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。

退出移动版