OMP培训文档¶
约 1402 个字 215 行代码 1 张图片 预计阅读时间 7 分钟
OpenMP入门¶
参考链接: OpenMP入门教程 系列文章
-
OpenMP概述
- 什么是OpenMP?
OpenMP(Open Multi-Processing)是一种应用程序接口(API),用于在共享内存的多处理器系统上进行并行编程。它通过在代码中添加指令(pragmas)的方式,让程序员能够简单地将串行代码并行化,而不需要深入底层硬件或复杂的多线程管理。OpenMP支持C、C++和Fortran等语言。
- 适用场景:共享内存并行模型
-
OpenMP编程模型
- 共享内存模型
共享内存模型指的是所有线程在同一个地址空间中共享数据。这意味着不同线程可以访问相同的内存位置,并且可以共享变量的值。
共享变量:在并行区域中,默认情况下,大多数变量是共享的,即所有线程都可以访问和修改这些变量的值。
私有变量:某些情况下,我们可能希望每个线程拥有变量的私有副本,这样不同线程之间不会相互干扰。OpenMP通过
private
指令指定这些变量。数据竞争(Race Condition):由于多个线程同时访问和修改共享变量,可能会导致数据竞争问题。为了避免这种情况,OpenMP提供了同步机制,如
critical
和atomic
等。- 并行区域(Parallel Region)
并行区域是OpenMP编程的核心概念。它是由编译器指令
#pragma omp parallel
指定的一段代码,告诉OpenMP在这段代码中创建多个线程并行执行。 -
OpenMP基本指令
#pragma omp parallel
用途: 定义一个并行区域,启动多个线程并行执行该区域中的代码。
示例:
#pragma omp parallel { // 并行执行的部分 }
#pragma omp for
用途: 将循环的迭代分配给多个线程并行执行。
示例:
#pragma omp parallel for for (int i = 0; i < n; i++) { // 并行执行的循环体 }
#pragma omp single
用途:指定代码块只由第一个到达线程执行,其他线程跳过该代码块。
-
OpenMP中的同步机制
#pragma omp critical
用途: 定义一个临界区,保证代码块在同一时刻只被一个线程执行,以防止竞争条件。
#pragma omp barrier
用途: 强制所有线程在此处同步,确保所有线程都执行到这一步后,才继续执行后续代码。
-
变量的作用域
shared
:默认情况下,并行区域外申明的变量在并行区域中是共享的,可以使用shared
子句显式指定变量为共享的。
示例:
int a; #pragma omp parallel for shared(a) for (int i = 0; i < n; i++) { // a为公有变量 }
private
:每个线程在并行区域中有自己独立的变量副本,线程之间相互独立,互不干扰。并行区域内申明的变量默认为私有的,并行区域外申明的变量需要显式申明private
示例:
int a; #pragma omp parallel for private(a) for (int i = 0; i < n; i++) { int b; //a,b均为私有变量 }
reduction
: 用于将每个线程的私有变量在并行区域结束时进行归约(如求和、求最大值等),最终将结果存储到共享变量中。
示例:
int sum = 0; #pragma omp parallel for reduction(+:sum) for (int i = 0; i < 10; i++) { sum += i; }
-
调度方法
-
static
:静态调度将循环的迭代均匀分配给所有线程,并且相邻的迭代会被分配在同一个线程,分配方式在程序开始执行时就已经确定。示例:
#pragma omp parallel for schedule(static, 3) for (int i = 0; i < n; i++) { // 每个线程执行3个连续的迭代 }
-
dynamic
:动态调度在执行时分配迭代,每当一个线程完成当前分配的迭代时,它会动态获取下一个块的迭代。 -
guided
:引导调度是一种动态调度的变体,但块大小(chunk size)随着任务的完成而逐渐减小。 -
auto
:自动调度将调度策略的选择权交给编译器或运行时库,由它们决定最佳的调度方式。 -
runtime
:运行时调度允许在程序运行时通过环境变量设置调度策略。
-
-
环境变量
- OMP_SCHEDULE:负责规定调度方式。
- OMP_NUM_THREADS:设置执行期间要使用的最大线程数。
- OMP_PROC_BIND:启用或禁用线程绑定到处理器。有效值为TRUE或FALSE。
- OMP_STACKSIZE:控制创建(非主)线程的堆栈大小。
OpenMP实验¶
关于程序运行
如果你要在ide(如codeblocks、vscode、vs)等地方运行,请信息检索如何配置环境。
如果你在命令行上运行,参考以下编译运行方法:
gcc -fopenmp {YOUR_CODE_NAME}.c -o {YOUR_CODE_NAME}
./{YOUR_CODE_NAME}
请将"{YOUR_CODE_NAME}"替换成你的文件名。
-
编写一个Hello World程序,尝试使用OpenMP并行输出,并且输出其线程id。多运行几次,观察输出结果的变化。
代码提示
#include <omp.h> #include <stdio.h> int main() { //==== TODO:创建并行区域 ==== { //==== TODO:获取线程id(具体方式请自行查阅) ==== printf("Hello from thread %d\n", tid); } return 0; }
-
编写一个进行矩阵计算的程序,并给它添加OpenMP并行,观察添加OpenMP前后的计算时间,有什么变化?
代码提示
#include <omp.h> #include <stdio.h> #include <stdlib.h> #include <string.h> const int N = 600; int main() { float A[N][N], B[N][N], C[N][N]; //初始化矩阵,如果你想,你可以把这里也并行了 for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { A[i][j] = 1.0f; B[i][j] = 1.0f; C[i][j] = 0.0f; } } double begin = omp_get_wtime(); //==== TODO:OpenMP并行 ==== for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { C[i][j] = 0; for (int k = 0; k < N; k++) { C[i][j] += A[i][k] * B[k][j]; } } } double end = omp_get_wtime()-begin; printf("计算时间: %.6f 秒\n", end); return 0; }
-
阅读并运行参考代码,观察不同调度方法下的运行时间。你可以尝试修改负载、任务总数等数据。
参考代码
#include <cstdio> #include <cmath> #include <cstdlib> #include <omp.h> // 模拟不同计算负载的函数 double compute_work(int i, bool balanced) { if (balanced) { // 均衡负载:所有任务计算量相同 return sqrt(i) + log(i + 100); } else { // 不均衡负载:计算量随任务ID变化 if (i % 10000 == 0) { // 每1000个任务有一个重负载任务 double result = 0.0; for (int j = 0; j < 500000; j++) { result += sqrt(i + j) * log(i + j + 1); } return result; } else { // 普通负载任务 return sqrt(i) + log(i + 100); } } } void run_experiment(bool balanced, const char* mode_name) { const int N = 10000000; // 任务总数 const int chunk = 100; // 任务块大小 double sum = 0.0; // 计算结果 printf("\n===== %s负载实验 =====\n", mode_name); // 静态调度 sum = 0.0; double start = omp_get_wtime(); #pragma omp parallel for reduction(+:sum) schedule(static, chunk) for (int i = 0; i < N; i++) { sum += compute_work(i, balanced); } double end = omp_get_wtime(); printf("Static : 时间 = %.4f秒\n", end - start); // 动态调度 sum = 0.0; start = omp_get_wtime(); #pragma omp parallel for reduction(+:sum) schedule(dynamic, chunk) for (int i = 0; i < N; i++) { sum += compute_work(i, balanced); } end = omp_get_wtime(); printf("Dynamic : 时间 = %.4f秒\n", end - start); // 引导调度 sum = 0.0; start = omp_get_wtime(); #pragma omp parallel for reduction(+:sum) schedule(guided, chunk) for (int i = 0; i < N; i++) { sum += compute_work(i, balanced); } end = omp_get_wtime(); printf("Guided : 时间 = %.4f秒\n", end - start); } int main() { // 设置线程数(可根据需要调整) omp_set_num_threads(4); printf("使用OpenMP线程数: %d\n", omp_get_max_threads()); // 运行负载均衡实验 run_experiment(true, "均衡"); // 运行负载不均衡实验 run_experiment(false, "不均衡"); return 0; }
-
蒙特卡洛算法估算\(\pi\)。尝试实现串行版本代码,并修改为openmp并行版本,在很大试验次数的情况下对比运行时间。“代码提示”中提供了串行版本的实现。
蒙特卡洛算法估算\(\pi\)
做法如下:在[0,1]内随机生成两个数字作为一个点的x,y坐标,计算该点到坐标原点的距离,判断距离是否小于1。如此重复很多很多次,记录下距离小于1的点的个数。用距离小于1的点的个数除以总实验次数,这个值将会是 \(\frac{\pi}{4}\)。(就是用频率估计概率+几何概型。)
代码提示
#include <omp.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #define NUM_POINTS 100000000 int main() { printf("总点数 = %d\n", NUM_POINTS); srand(time(NULL)); double start_time, end_time; double pi_estimate; int num_inside; // ============串行版本============ start_time = omp_get_wtime(); num_inside = 0; for (long i = 0; i < NUM_POINTS; i++) { double x = (double)rand() / RAND_MAX; double y = (double)rand() / RAND_MAX; if (x*x + y*y <= 1.0) { num_inside++; } } pi_estimate = 4.0 * num_inside / NUM_POINTS; end_time = omp_get_wtime(); printf("\n串行版本:\n"); printf("π估计值 = %.8f\n", pi_estimate); printf("计算时间 = %.6f 秒\n", end_time - start_time); // ============ TODO:并行版本 ============ num_inside = 0; start_time = omp_get_wtime(); //... //... //... //... pi_estimate = 4.0 * num_inside / NUM_POINTS; end_time = omp_get_wtime(); printf("\n并行版本:\n"); printf("π估计值 = %.8f\n", pi_estimate); printf("计算时间 = %.6f 秒\n", end_time - start_time); return 0; }