跳转至

OMP培训文档

约 1402 个字 215 行代码 1 张图片 预计阅读时间 7 分钟

OpenMP入门

参考链接: OpenMP入门教程 系列文章


  1. OpenMP概述

    • 什么是OpenMP?

    OpenMP(Open Multi-Processing)是一种应用程序接口(API),用于在共享内存的多处理器系统上进行并行编程。它通过在代码中添加指令(pragmas)的方式,让程序员能够简单地将串行代码并行化,而不需要深入底层硬件或复杂的多线程管理。OpenMP支持C、C++和Fortran等语言。

    • 适用场景:共享内存并行模型
  2. OpenMP编程模型

    • 共享内存模型

    共享内存模型指的是所有线程在同一个地址空间中共享数据。这意味着不同线程可以访问相同的内存位置,并且可以共享变量的值。

    共享变量:在并行区域中,默认情况下,大多数变量是共享的,即所有线程都可以访问和修改这些变量的值。

    私有变量:某些情况下,我们可能希望每个线程拥有变量的私有副本,这样不同线程之间不会相互干扰。OpenMP通过private指令指定这些变量。

    数据竞争(Race Condition):由于多个线程同时访问和修改共享变量,可能会导致数据竞争问题。为了避免这种情况,OpenMP提供了同步机制,如criticalatomic等。

    • 并行区域(Parallel Region)

    并行区域是OpenMP编程的核心概念。它是由编译器指令#pragma omp parallel指定的一段代码,告诉OpenMP在这段代码中创建多个线程并行执行。

  3. OpenMP基本指令

    • #pragma omp parallel

    用途: 定义一个并行区域,启动多个线程并行执行该区域中的代码。

    示例

    #pragma omp parallel
    {
        // 并行执行的部分
    }
    
    • #pragma omp for

    用途: 将循环的迭代分配给多个线程并行执行。

    示例

    #pragma omp parallel for
    for (int i = 0; i < n; i++) {
        // 并行执行的循环体
    }
    
    • #pragma omp single

    用途:指定代码块只由第一个到达线程执行,其他线程跳过该代码块。

  4. OpenMP中的同步机制

    • #pragma omp critical

    用途: 定义一个临界区,保证代码块在同一时刻只被一个线程执行,以防止竞争条件。

    • #pragma omp barrier

    用途: 强制所有线程在此处同步,确保所有线程都执行到这一步后,才继续执行后续代码。

  5. 变量的作用域

    • 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;
    }
    
  6. 调度方法

    • static:静态调度将循环的迭代均匀分配给所有线程,并且相邻的迭代会被分配在同一个线程,分配方式在程序开始执行时就已经确定。

      示例:

      #pragma omp parallel for schedule(static, 3)
      for (int i = 0; i < n; i++) {
          // 每个线程执行3个连续的迭代
      }
      
    • dynamic:动态调度在执行时分配迭代,每当一个线程完成当前分配的迭代时,它会动态获取下一个块的迭代。

    • guided:引导调度是一种动态调度的变体,但块大小(chunk size)随着任务的完成而逐渐减小。

    • auto:自动调度将调度策略的选择权交给编译器或运行时库,由它们决定最佳的调度方式。

    • runtime:运行时调度允许在程序运行时通过环境变量设置调度策略。

  7. 环境变量

    • 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}"替换成你的文件名。

  1. 编写一个Hello World程序,尝试使用OpenMP并行输出,并且输出其线程id。多运行几次,观察输出结果的变化。

    代码提示
    #include <omp.h>
    #include <stdio.h>
    
    int main()
    {
        //==== TODO:创建并行区域 ====
        {
            //==== TODO:获取线程id(具体方式请自行查阅) ====
    
            printf("Hello from thread %d\n", tid);
    
        }
        return 0;
    }
    
  2. 编写一个进行矩阵计算的程序,并给它添加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;
    }
    
  3. 阅读并运行参考代码,观察不同调度方法下的运行时间。你可以尝试修改负载、任务总数等数据。

    参考代码
    #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;
    }
    
  4. 蒙特卡洛算法估算\(\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;
    }