目录

OpenMP 学习手册

OpenMP(Open Multi-Processing)是一个用于多线程并行编程的应用程序接口(API),允许程序员通过添加特定的编译指示(pragma)来并行化程序代码,而无需大幅修改源代码。

这些编译指示可以在编译时被编译器解析并转化为并行执行的代码,如果忽略这些指示(pragma),程序仍然可以退化为串行执行,完全不影响基础功能,兼具灵活性与兼容性。在此基础上,OpenMP 还具备三大核心优势:

  • 降低并行编程难度:提供高层抽象,减少对具体实现细节的关注。
  • 良好的可移植性:支持多种编译器(如 GCC、Clang、VS)和操作系统(Unix/Linux 和 Windows)。
  • 灵活的并行控制:支持多种并行构造和同步机制,满足不同场景需求。

OpenMP 采用“主线程 + 多个工作线程”模型。在程序执行过程中,主线程负责管理并行任务的调度和同步,多个工作线程则并行执行指定的任务。程序从一个单一的主线程开始执行,当遇到并行区域时,主线程会创建多个子线程,形成一个线程团队。这些线程团队中的线程会并行地执行代码,直到并行区域结束,线程团队中的线程会同步并终止,仅保留主线程继续执行后续代码。这种模型被称为 Fork-Join 模型。

OpenMP 采用共享内存模型,所有线程都可以访问共享内存中的数据。程序员可以通过 OpenMP 的编译指令和运行时库函数来控制线程的行为,实现并行计算。

并行区域是 OpenMP 编程中最基础的并行构造,通过 #pragma omp parallel 指令来定义。可以在并行区域内包含多个线程私有变量、共享变量、循环并行、任务并行等。

#pragma omp parallel [clause ...]
{
    // 并行区域内的代码
}

在并行区域内,变量作用域子句用于控制变量的作用域和初始值。

  • private:声明线程私有变量,每个线程拥有自己的副本,初始值未定义。
  • firstprivate:线程私有变量,初始值为主线程中的值。
  • lastprivate:循环结束后,变量值取循环最后一次迭代的值。
  • reduction:对变量执行归约操作(如求和、最大值、最小值等)。
#include <iostream>
#include <omp.h>

int main() {
    int a = 10;
    #pragma omp parallel private(a)
    {
        a = omp_get_thread_num();
        std::cout << "Thread " << omp_get_thread_num() << " private a: " << a << std::endl;
    }

    a = 10;
    #pragma omp parallel firstprivate(a)
    {
        a = omp_get_thread_num() + a;
        std::cout << "Thread " << omp_get_thread_num() << " firstprivate a: " << a << std::endl;
    }

    int b = 0;
    #pragma omp parallel for lastprivate(b)
    for (int i = 0; i < 4; i++) {
        b = i;
        std::cout << "Thread " << omp_get_thread_num() << " iteration " << i << std::endl;
    }
    std::cout << "Lastprivate b: " << b << std::endl;

    return 0;
}

输出结果:

Thread 0 private a: 0
Thread 1 private a: 1
Thread 0 firstprivate a: 10
Thread 1 firstprivate a: 11
Thread 0 iteration 0
Thread 1 iteration 1
Thread 0 iteration 2
Thread 1 iteration 3
Lastprivate b: 3

循环并行化是 OpenMP 中最常见的并行构造,通过 #pragma omp for 指令来实现,可以并行化 for 循环、while 循环和 do-while 循环。

#pragma omp for [clause ...]

schedule 子句用于控制循环迭代的分配方式,决定每个线程处理的任务块大小和分配策略。常见的调度类型包括:

  • static:循环迭代被静态地分配给各个线程,块大小固定,适用于迭代数较大且每个迭代成本较小的情况。
  • dynamic:循环迭代被动态地分配给各个线程,线程完成一个块后会获取新的块,适用于迭代数较小时。
  • guided:初始块大小较大,后续块大小逐渐减小,适用于迭代数较大且每个迭代成本较小时。
  • runtime:循环的并行化方式在运行时根据环境变量 OMP_SCHEDULE 动态确定。
  • auto:调度决策由编译器/运行时系统决定。
#include <iostream>
#include <omp.h>

int main() {
    int n = 10;
    #pragma omp parallel for schedule(static, 2)
    for (int i = 0; i < n; i++) {
        int tid = omp_get_thread_num();
        std::cout << "Thread " << tid << " is processing iteration " << i << std::endl;
    }
    return 0;
}

输出结果:

Thread 0 is processing iteration 0
Thread 0 is processing iteration 1
Thread 1 is processing iteration 2
Thread 1 is processing iteration 3
Thread 2 is processing iteration 4
Thread 2 is processing iteration 5
Thread 3 is processing iteration 6
Thread 3 is processing iteration 7
Thread 0 is processing iteration 8
Thread 0 is processing iteration 9

分区块用于并行执行多个独立的任务,通过 #pragma omp sections 指令来定义。

#pragma omp sections [clause ...]
{
    #pragma omp section
    {
        // 分区块内的代码
    }
}
  • nowait:省略分区块结束后的隐式屏障同步,允许线程在完成各自的任务后立即继续执行后续代码,而无需等待其他线程完成。
#include <iostream>
#include <omp.h>

int main() {
    #pragma omp parallel
    {
        #pragma omp sections nowait
        {
            #pragma omp section
            {
                int tid = omp_get_thread_num();
                std::cout << "Thread " << tid << " is executing section 1" << std::endl;
            }
            #pragma omp section
            {
                int tid = omp_get_thread_num();
                std::cout << "Thread " << tid << " is executing section 2" << std::endl;
            }
        }
        std::cout << "Thread " << omp_get_thread_num() << " is done with sections" << std::endl;
    }
    return 0;
}

输出结果:

Thread 0 is executing section 1
Thread 0 is done with sections
Thread 1 is executing section 2
Thread 1 is done with sections

OpenMP 提供了锁机制用于保护共享资源,避免竞争条件。常见的锁类型包括:

  • omp_set_lock:设置锁。
  • omp_unset_lock:释放锁。
  • omp_test_lock:测试锁是否可用。
#include <iostream>
#include <omp.h>

int main() {
    int x = 0;
    omp_lock_t lock;
    omp_init_lock(&lock);

    #pragma omp parallel
    {
        #pragma omp critical
        {
            omp_set_lock(&lock);
            x++;
            std::cout << "Thread " << omp_get_thread_num() << " incremented x to " << x << std::endl;
            omp_unset_lock(&lock);
        }
    }

    omp_destroy_lock(&lock);
    return 0;
}

输出结果:

Thread 0 incremented x to 1
Thread 1 incremented x to 2
Thread 2 incremented x to 3

在这个示例中,多个线程尝试访问共享变量 x,通过锁机制确保每次只有一个线程能够修改 x,避免了竞争条件。

OpenMP 提供了任务并行机制,通过 #pragma omp task 指令来定义任务,允许程序员显式地定义并行任务,灵活地控制任务的创建、调度和执行。

#pragma omp task [clause ...]
{
    // 任务内的代码
}
#include <iostream>
#include <omp.h>

int main() {
    #pragma omp parallel
    {
        #pragma omp single
        {
            int a = 0, b = 1;
            #pragma omp task
            {
                a = a + b;
            }
            std::cout << "a = " << a << std::endl;
        }
    }
    return 0;
}

任务的调度由 OpenMP 运行时库自动管理,但可以通过 schedule 子句进行控制。

#pragma omp task schedule(schedule_type)
#include <iostream>
#include <omp.h>

int main() {
    #pragma omp parallel
    {
        int a = 0, b = 1;
        #pragma omp task schedule(dynamic)
        {
            a = a + b;
        }
        std::cout << "a = " << a << std::endl;
    }
    return 0;
}

任务之间的同步可以通过 #pragma omp barrier 指令实现,确保所有任务完成前后续代码不会执行。

#pragma omp barrier
#include <iostream>
#include <omp.h>

int main() {
    #pragma omp parallel
    {
        int a = 0, b = 1;
        #pragma omp task
        {
            a = a + b;
        }
        #pragma omp task
        {
            std::cout << "a = " << a << std::endl;
        }
        #pragma omp barrier
        std::cout << "All tasks completed." << std::endl;
    }
    return 0;
}

OpenMP 常用于科学计算中的矩阵运算、数值模拟等场景。通过并行化循环和任务分配,可以显著加速计算过程。

#include <iostream>
#include <omp.h>

int main() {
    const int N = 1000;
    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.0;
            b[i][j] = 2.0;
        }
    }

    // 矩阵相加
    #pragma omp parallel for
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            c[i][j] = a[i][j] + b[i][j];
        }
    }

    return 0;
}

在大规模数据处理任务中,OpenMP 可以并行化数据的读取、处理和写入,提高数据处理效率。

#include <iostream>
#include <omp.h>
#include <vector>

int main() {
    const int N = 1000000;
    std::vector<float> data(N);

    // 初始化数据
    for (int i = 0; i < N; i++) {
        data[i] = static_cast<float>(i);
    }

    // 并行处理数据
    #pragma omp parallel for
    for (int i = 0; i < N; i++) {
        data[i] = data[i] * data[i];
    }

    return 0;
}

OpenMP 可用于加速机器学习算法中的训练过程,如并行化梯度计算、损失函数评估等。

#include <iostream>
#include <omp.h>
#include <vector>

int main() {
    const int N = 1000000;
    std::vector<float> features(N);
    std::vector<float> labels(N);
    std::vector<float> gradients(N, 0.0);

    // 初始化数据
    for (int i = 0; i < N; i++) {
        features[i] = static_cast<float>(i);
        labels[i] = features[i] * 2 + 1;
    }

    // 并行计算梯度
    #pragma omp parallel for
    for (int i = 0; i < N; i++) {
        float prediction = features[i] * 2;
        gradients[i] = prediction - labels[i];
    }

    return 0;
}
  • 《OpenMP 编程指南》
  • 《Using OpenMP: Portable Shared Memory Parallel Programming》

OpenMP 作为一种高效的并行编程工具,通过简单的编译指令和运行时库函数,使得程序员能够轻松地实现程序的并行化。通过合理利用 OpenMP 的各种特性,可以显著提高程序的性能,充分发挥多核处理器的计算能力。