MINIBLOG

Blog Note Tags Links About
Home Search
Jun 2, 2026
miniyuan

OpenMP 编程模型


核心概念与背景

从串行到并行

程序是运行在计算机上的指令流。在摩尔定律的引领下,程序员长期习惯于编写适用于顺序计算机的程序。然而,随着摩尔定律走向终结,硬件工程师将越来越复杂的并行结构交给软件开发者。并行程序是同时在多个处理单元上运行多个指令流,其基本思想简单,但正确使用并行计算解决实际问题并不简单。

关键理解:并行计算中的许多理念(如共享内存、线程同步、数据竞争)对于传统顺序编程是陌生的。在 AI 与 HPC 时代,OpenMP、MPI、CUDA 等并行方式依然不可替代。

进程、线程与共享内存

在操作系统中,**进程(process)**是资源分配的基本单位,拥有独立的存储空间;**线程(thread)**是调度执行的基本单位,可视为进程的一部分,一个进程可以开启多个线程。

特性进程(Process)线程(Thread)
内存空间私有,相互独立共享同一进程的内存
通信方式需显式消息传递(如 MPI 的 Send/Recv)直接读写共享变量即可”通信”
创建/调度开销大小
并行模型分布式内存(Distributed Memory)共享内存(Shared Memory)

OpenMP(Open Multi-Processing - 开放多处理)正是基于共享内存的多线程并行编程接口。它包含编译制导指令、库函数和环境变量三部分,支持 Fortran、C/C++ 等多种语言,具有良好的可移植性。


Fork-Join 并行执行模式

模式概述

OpenMP 主要采用 Fork-Join(分叉-合并)模式进行并行:

  1. 程序开始时只有一个**主线程(Master Thread)**运行串行代码
  2. 当遇到 #pragma omp parallel 时,主线程**分叉(Fork)**出一组线程,共同执行并行区域内的代码
  3. 并行区域结束后,所有线程合并(Join),仅主线程继续执行后续串行代码
  4. 该模式可在程序中反复出现,形成”串行区 → 并行区 → 串行区 → 并行区”的交替结构

并发性与执行交错

并发(Concurrency) 并不意味着严格的”同时执行”,而是指多个指令流之间的指令相互无序。编译时通过 -fopenmp 告诉编译器创建多线程程序。

⚠️ 注意事项:每个线程内部的 cout 语句遵循程序定义的顺序,但在不同线程之间没有指定顺序。因此每次运行的输出交错方式可能都不一样。作为程序员,必须确保所有可能的执行交错都能产生正确结果。


OpenMP 编程三要素

OpenMP 程序由串行程序通过以下三个组成部分改造而来:

串行程序 ──→ [ 编程指导语句 ] ──→ 并行程序
             [   库函数    ]  ↑
             [  环境变量   ] ──┘

编译制导指令(Programming Directives)

#pragma(源于希腊语,意为”行动/事务”)在计算机科学中用于指代编译器指令。在 OpenMP 中,它与特定关键字组合,指导编译器如何在并发环境中执行代码。

C/C++ 中 OpenMP 指令的基本格式:

#pragma omp directive-name [clause[[,] clause]...]

核心指令:

  • #pragma omp parallel(parallel - 并行区域,创建一组线程执行结构化块)

课件中表格还提及了其他常见指令(后续课程展开):

  • #pragma omp for(for - 循环工作共享,将循环迭代分配给各线程)
  • #pragma omp barrier(barrier - 屏障,线程在此等待直到所有线程到达)
  • #pragma omp critical(critical - 临界区,同一时刻仅允许一个线程执行)
  • #pragma omp parallel for(parallel for - 并行区域与循环工作共享的合并写法)
  • #pragma omp single(single - 仅由单个线程执行)
  • #pragma omp task(task - 任务,异步执行单元)

库函数(Library Functions)

OpenMP 提供辅助编程的 API,程序员可通过调用这些函数获取运行时信息。

库函数英文全称解析功能说明
omp_set_num_threads(n)set number of threads - 设置线程数设置后续并行区域的默认线程数,直到被新值覆盖
omp_get_thread_num()get thread number - 获取线程编号返回当前线程在团队中的 ID,主线程为 0,依次递增
omp_get_num_threads()get number of threads - 获取线程总数返回当前并行区域中的线程总数
omp_get_wtime()get wall time - 获取墙上时钟时间返回自过去某一时刻起经过的秒数,用于性能计时
omp_get_num_procs()get number of processors - 获取处理器核心数返回程序可用的处理器核心数量

环境变量(Environment Variables)

环境变量与库函数在功能上通常对应,用于在程序外部控制 OpenMP 行为。

环境变量英文全称解析功能说明
OMP_NUM_THREADSOpenMP Number of Threads - 线程数量设置并行区域的默认线程数
OMP_SCHEDULEOpenMP Schedule - 调度策略设置循环的默认调度策略(如 static、dynamic 等)

最佳实践提示:在 Linux 中通过 export OMP_NUM_THREADS=10 设置环境变量,使用 echo $OMP_NUM_THREADS 查看,使用 unset OMP_NUM_THREADS 清除。 若清除环境变量且程序中未显式设置,则默认使用所有逻辑 CPU(如 56 个线程)。


代码示例

示例一:Hello World(串行 vs 并行)

串行版本:

#include <iostream>

int main() {
    std::cout << "Hello 1" << std::endl;
    std::cout << "Hi" << std::endl;
    std::cout << "Hello 2" << std::endl;
    return 0;
}

并行版本:

#include <iostream>
#include <omp.h>  // OpenMP 头文件

int main() {
    std::cout << "Hello 1" << std::endl;  // 串行区:仅主线程执行

    #pragma omp parallel  // 进入并行区域(Fork)
    {
        std::cout << "Hi" << std::endl;  // 并行区:所有线程各执行一次
    }  // 并行区域结束(Join)

    std::cout << "Hello 2" << std::endl;  // 串行区:仅主线程执行
    return 0;
}

编译与运行:

g++ -fopenmp hello_world_openmp.cpp -o hello_world_openmp
./hello_world_openmp

常见错误: 若期望看到 5 行整齐的 Hello world,实际输出可能是交错的 Hello worldHelloHello world。 这是因为多个线程同时向标准输出写入,产生了数据竞争(Race Condition)。omp_get_thread_num() 可以帮助区分不同线程的输出。

示例二:利用线程 ID 分离输出

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

int main() {
    using namespace std;

    const int nthreads = 4;
    omp_set_num_threads(nthreads);  // 设置线程数为 4

    #pragma omp parallel
    {
        stringstream ss;
        
        // 获取当前线程 ID(从 0 开始)
        const int id = omp_get_thread_num();  // get thread number
        ss << "Log_" << id + 1 << ".txt";
        
        // 每个线程独立写入自己的文件,避免输出交错
        ofstream ofs(ss.str().c_str());
        ofs << "Hello from thread " << id << endl;
        ofs.close();
    }

    return 0;
}

执行逻辑:

  • 4 个线程分别创建 Log_1.txt ~ Log_4.txt
  • 每个文件内记录对应线程的 ID,彻底避免了标准输出的竞争问题

对比与总结

串行执行 vs Fork-Join 并行执行

阶段串行程序OpenMP 并行程序
开始单线程依次执行所有语句主线程执行串行区
并行区入口无主线程 Fork 出多个子线程
并行区内无所有线程(含主线程)同时执行相同代码块
并行区出口无所有子线程 Join,仅主线程继续
结束单线程结束主线程继续执行后续串行区

设置线程数的三种方式优先级

方式示例说明
环境变量export OMP_NUM_THREADS=8全局设置,影响所有未显式指定的并行区
库函数omp_set_num_threads(4)程序内设置,覆盖环境变量,影响后续并行区
指令子句#pragma omp parallel num_threads(2)仅影响当前并行区(最细粒度,优先级最高)

思考题:如果环境变量设置为 8,程序中 omp_set_num_threads(4),而指令上又有 num_threads(2),最终线程数是多少?(答案:2,就近原则)

目录
  • 核心概念与背景
    • 从串行到并行
    • 进程、线程与共享内存
  • Fork-Join 并行执行模式
    • 模式概述
    • 并发性与执行交错
  • OpenMP 编程三要素
    • 编译制导指令(Programming Directives)
    • 库函数(Library Functions)
    • 环境变量(Environment Variables)
  • 代码示例
    • 示例一:Hello World(串行 vs 并行)
    • 示例二:利用线程 ID 分离输出
  • 对比与总结
    • 串行执行 vs Fork-Join 并行执行
    • 设置线程数的三种方式优先级
© 2026 miniyuan. All rights reserved.
Go to miniyuan's GitHub repo