- 文档
- 教程
【免费下载链接】learnxinyminutes-docs
Code documentation written as code! How novel and totally my idea!
本文基于 Learn X in Y minutes(learnxinyminutes-docs)仓库中的 openmp.md,以该文档的完整脉络为主线,系统讲解 OpenMP 共享内存并行编程:从 Master/Slave 线程模型与编译开关,到omp.h运行时 API、私有/共享变量声明、七类同步指令,再到循环并行化、加速比实测与分形计算实战。读完本文,你可以独立完成 OpenMP 程序的编写、编译、线程数调优与并行加速验证。
一、OpenMP 是什么
OpenMP 是用于共享内存机器的并行编程库。它允许你使用简单的高层语法结构(pragma 指令)来表达并行性,同时把底层线程管理的细节隐藏起来,做到易用、编写快速。OpenMP 支持 C、C++ 和 Fortran 三种语言。
在这个文档仓库的定位体系中,OpenMP 并不是"编程语言",而是被归类为工具类教程。可以从 openmp.md 开头的 frontmatter 直接验证这一点:
--- category: tool name: OpenMP filename: learnopenMP.cpp contributors: - ["Cillian Smith", "https://github.com/smithc36-tcd"] ---对照 CONTRIBUTING.md 的规范:category字段可取language、tool或Algorithms & Data Structures三类(缺省为language);filename字段指定了本篇教程代码的汇总文件名,站点构建时会据此把正文中的代码合并成一个可下载的文件(即learnopenMP.cpp)。而 lint/frontmatter.py 中的allowed_keys校验列表(name、category、filename、contributors、translators等)保证了这类 frontmatter 在仓库层面通过格式检查。
二、线程模型:Master 与 Slave
一个典型的 OpenMP 程序遵循如下结构(引自 openmp.md):
- Master(主线程):负责启动环境、初始化变量;
- Slave(从线程):为被特殊指令(directive)标记的代码段而创建,真正执行并行部分的线程。
原文档用一张 ASCII 图刻画了主从结构:
__________ Slave /__________ Slave / Master ------------- Master \___________ Slave \__________ Slave每个线程都有唯一的 ID,可通过omp_get_thread_num()获取,下文会详细展开。
三、第一个 OpenMP 程序:编译与运行
最简单的 "hello world" 程序可以通过#pragma omp parallel指令并行化:
#include <stdio.h> int main() { #pragma omp parallel { printf("Hello, World!\n"); } return 0; }关键点在于编译时必须打开 OpenMP 开关,而开关取决于具体编译器(这是原文档明确提示的):
| 编译器 | 编译参数 |
|---|---|
| Intel | -openmp |
| GCC | -fopenmp |
| pgcc | -mp |
以 GCC 为例:
gcc -fopenmp hello.c -o Hello运行后输出形如:
Hello, World! ... Hello, World!"Hello, World!" 的打印次数取决于机器的核心数,原文档作者在自己的笔记本上得到了 12 次。换句话说:不加任何控制时,omp parallel区域默认会派生出与处理器核数相匹配的一组线程,每个线程各打印一次。
四、线程控制与omp.h运行时 API
4.1 用环境变量控制线程数
最直接的方式是设置环境变量:
export OMP_NUM_THREADS=8这样即可把默认线程数改为 8,无需修改任何代码。
4.2omp.h中的常用库函数
omp.h头文件提供了一组运行时函数,用于查询和调整线程状态(完整清单来自 openmp.md):
// Check the number of threads printf("Max Threads: %d\n", omp_get_max_threads()); printf("Current number of threads: %d\n", omp_get_num_threads()); printf("Current Thread ID: %d\n", omp_get_thread_num()); // Modify the number of threads omp_set_num_threads(int); // Check if we are in a parallel region omp_in_parallel(); // Dynamically vary the number of threads omp_set_dynamic(int); omp_get_dynamic(); // Check the number of processors printf("Number of processors: %d\n", omp_num_procs());这些函数按用途可分为三类,整理如下:
| 函数 | 作用 | 类别 |
|---|---|---|
omp_get_max_threads() | 查询当前最大线程数 | 查询 |
omp_get_num_threads() | 查询当前(所在并行区域内)线程数 | 查询 |
omp_get_thread_num() | 查询当前线程 ID | 查询 |
omp_num_procs() | 查询处理器(核)数量 | 查询 |
omp_in_parallel() | 判断当前是否处于并行区域内 | 查询 |
omp_set_num_threads(int) | 设置线程数 | 控制 |
omp_set_dynamic(int)/omp_get_dynamic() | 开关/查询"动态调整线程数"行为 | 控制 |
其中omp_in_parallel()常用于区分主线程与从线程的执行路径;omp_set_dynamic与omp_get_dynamic构成一对开关接口,允许运行时动态伸缩线程数量(原文档将其归纳为 "Dynamically vary the number of threads")。
五、私有变量与共享变量
并行区域内变量可以被声明为私有(private)或共享(shared)(原文示例见 openmp.md):
// Variables in parallel sections can be either private or shared. /* Private variables are private to each thread, as each thread has its own * private copy. These variables are not initialized or maintained outside * the thread. */ #pragma omp parallel private(x, y) /* Shared variables are visible and accessible by all threads. By default, * all variables in the work sharing region are shared except the loop * iteration counter. * * Shared variables should be used with care as they can cause race conditions. */ #pragma omp parallel shared(a, b, c) // They can be declared together as follows #pragma omp parallel private(x, y) shared(a,b,c)要点归纳:
- 私有变量:每个线程持有独立副本,互不干扰;线程之外不初始化、不维护,生命周期只存在于该线程内。私有变量天然不存在竞争。
- 共享变量:所有线程可见、可访问。默认规则是——除循环迭代计数器外,工作共享区域中的变量默认都是共享的;因此循环索引不会与外层变量冲突,但其他全局/外部变量都会。
- 共享变量必须小心使用,因为它们可能引发竞态条件(race condition),需要配合第六节的同步指令来保护。
private与shared子句可以在同一条指令中并列书写。
六、同步指令:critical、single、atomic、ordered、barrier、nowait、reduction
OpenMP 提供了一组指令来控制线程同步。原文档在同一个并行区域内集中演示了全部七种写法(完整代码见 openmp.md):
#pragma omp parallel { /* `critical`: the enclosed code block will be executed by only one thread * at a time, and not simultaneously executed by multiple threads. It is * often used to protect shared data from race conditions. */ #pragma omp critical data += data + computed; /* `single`: used when a block of code needs to be run by only a single * thread in a parallel section. Good for managing control variables. */ #pragma omp single printf("Current number of threads: %d\n", omp_get_num_threads()); /* `atomic`: Ensures that a specific memory location is updated atomically * to avoid race conditions. */ #pragma omp atomic counter += 1; /* `ordered`: the structured block is executed in the order in which * iterations would be executed in a sequential loop */ #pragma omp for ordered for (int i = 0; i < N; ++i) { #pragma omp ordered process(data[i]); } /* `barrier`: Forces all threads to wait until all threads reach this point * before proceeding. */ #pragma omp barrier /* `nowait`: Allows threads to proceed with their next task without waiting * for other threads to complete the current one. */ #pragma omp for nowait for (int i = 0; i < N; ++i) { process(data[i]); } /* `reduction` : Combines the results of each thread's computation into a * single result. */ #pragma omp parallel for reduction(+:sum) for (int i = 0; i < N; ++i) { sum += a[i] * b[i]; } }按语义逐条拆解:
| 指令 | 作用 | 典型用途 |
|---|---|---|
critical | 被包围的代码块同一时刻只允许一个线程执行 | 保护共享数据免遭竞态,是通用的互斥手段 |
single | 并行区域内仅由一个线程执行该代码块 | 管理控制变量、避免重复输出(如只打印一次状态) |
atomic | 保证对某一内存位置的更新是原子的 | 简单计数器等轻量共享更新,比critical开销更小 |
ordered | 结构化块按顺序循环的执行次序依次执行(需与for联用,块内以#pragma omp ordered标记) | 结果必须按迭代顺序产生/输出的场景 |
barrier | 强制所有线程等待,直到全部线程到达该点后才继续 | 阶段间同步 |
nowait | 线程无需等待其他线程完成当前工作即可进入下一任务 | 消除工作共享构造末尾的隐式屏障,降低同步开销 |
reduction | 把每个线程的局部计算结果归约为单一结果(如reduction(+:sum)) | 求和、求最值等并行归约 |
barrier 完整示例
原文档给出了一个最小可运行的barrier示例(openmp.md),它同时演示了omp_get_num_threads()、omp_get_thread_num()两个 API 的用法:
#include <omp.h> #include <stdio.h> int main() { // Current number of active threads printf("Num of threads is %d\n", omp_get_num_threads()); #pragma omp parallel { // Current thread ID printf("Thread ID: %d\n", omp_get_thread_num()); #pragma omp barrier <--- Wait here until other threads have returned if(omp_get_thread_num() == 0) { printf("\nNumber of active threads: %d\n", omp_get_num_threads()); } } return 0; }注意#pragma omp barrier后面那句"Wait here until other threads have returned"是原文档对指令位置的标注(不是代码的一部分,实际编译时应去掉)。示例逻辑是:主线程先打印当前线程数;进入并行区域后每个线程打印自己的 ID;barrier保证所有线程都打印完 ID 之后,才由 0 号线程打印活动线程总数。
七、循环并行化与数据依赖约束
用工作共享(work-sharing)指令并行化循环非常直接:
#pragma omp parallel { #pragma omp for // for loop to be parallelized for() ... }必须强调原文档给出的前提条件(openmp.md):
- 循环必须易于并行化,OpenMP 才能将其展开并在各线程间分配迭代;
- 如果相邻迭代之间存在数据依赖(data dependencies),OpenMP 无法并行化该循环——因为某次迭代的结果会依赖前一次迭代尚未完成的结果,强行拆分会破坏正确性。
八、串行 vs 并行:加速比实测
原文档提供了一个 C++ 程序,在同一台机器上对比串行循环与#pragma omp parallel for的执行时间(完整代码见 openmp.md):
#include <iostream> #include <vector> #include <ctime> #include <chrono> #include <omp.h> int main() { const int num_elements = 1e8; std::vector<double> a(num_elements, 1.0); std::vector<double> b(num_elements, 2.0); std::vector<double> c(num_elements, 0.0); // Serial version auto start_time = std::chrono::high_resolution_clock::now(); for (int i = 0; i < num_elements; i++) { c[i] = a[i] * b[i]; } auto end_time = std::chrono::high_resolution_clock::now(); auto duration_serial = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count(); // Parallel version with OpenMP start_time = std::chrono::high_resolution_clock::now(); #pragma omp parallel for for (int i = 0; i < num_elements; i++) { c[i] = a[i] * b[i]; } end_time = std::chrono::high_resolution_clock::now(); auto duration_parallel = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count(); std::cout << "Serial execution time: " << duration_serial << " ms" << std::endl; std::cout << "Parallel execution time: " << duration_parallel << " ms" << std::endl; std::cout << "Speedup: " << static_cast<double>(duration_serial) / duration_parallel << std::endl; return 0; }注:原文代码写作
100000000,即一亿个元素,上面保持数值等价;三组std::vector<double>分别初始化为 1.0 / 2.0 / 0.0。
原文档记录的运行结果为:
Serial execution time: 488 ms Parallel execution time: 148 ms Speedup: 3.2973原文档同时给出了两点诚实的保留意见,做性能评估时同样应当记住:
- 该例相当"构造化"(contrived),真实加速比取决于具体实现;
- 由于缓存性能(cache performance)等原因,串行代码有时反而可能比并行代码更快——并行化并非无条件加速,
omp parallel for的调度、线程间通信和缓存行为都会影响最终表现。
九、完整实战:OpenMP 计算 Mandelbrot 集合
原文档的收官示例用 OpenMP 计算 2000×2000 的 Mandelbrot 集合并输出 PPM 图像(完整代码见 openmp.md):
#include <iostream> #include <fstream> #include <complex> #include <vector> #include <omp.h> const int width = 2000; const int height = 2000; const int max_iterations = 1000; int mandelbrot(const std::complex<double> &c) { std::complex<double> z = c; int n = 0; while (abs(z) <= 2 && n < max_iterations) { z = z * z + c; n++; } return n; } int main() { std::vector<std::vector<int>> values(height, std::vector<int>(width)); // Calculate the Mandelbrot set using OpenMP #pragma omp parallel for schedule(dynamic) for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double real = (x - width / 2.0) * 4.0 / width; double imag = (y - height / 2.0) * 4.0 / height; std::complex<double> c(real, imag); values[y][x] = mandelbrot(c); } } // Prepare the output image std::ofstream image("mandelbrot_set.ppm"); image << "P3\n" << width << " " << height << " 255\n"; // Write the output image in serial for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int value = values[y][x]; int r = (value % 8) * 32; int g = (value % 16) * 16; int b = (value % 32) * 8; image << r << " " << g << " " << b << " "; } image << "\n"; } image.close(); std::cout << "Mandelbrot set image generated as mandelbrot_set.ppm." << std::endl; return 0; }该示例集中体现了前文多个知识点,值得逐点对照:
- 并行化对象的选择:只有最外层按行
y的循环被#pragma omp parallel for并行化。各行之间没有数据依赖(每行的values[y][x]只写自己那一行),满足第七节的"可并行化"前提;行内的x循环则保持串行。 schedule(dynamic)子句:Mandelbrot 各行迭代代价并不均匀——靠近分形边界的像素迭代次数接近max_iterations = 1000,外部点则很快逃逸。动态调度让先完成空闲的线程从剩余行中动态领取工作,从源码结构看这是针对"各行计算量不均"的合理选择,可以推断静态均分会造成线程负载不均衡。- 结果写入保持串行:输出阶段用单个
std::ofstream顺序写 PPM 文件,避免了多线程同时写同一个输出流的同步问题——这也呼应了"共享资源要保护/串行化"的原则。 - 图像格式:文件头
"P3\n2000 2000 255\n"是 PPM 文本格式的头部(宽 高 最大值),随后逐像素写 R、G、B;配色由迭代次数value取模生成,value越大的区域颜色变化越丰富。 - 产物:运行后生成
mandelbrot_set.ppm,并在控制台提示生成完成。
十、回到仓库:这篇文档在 learnxinyminutes-docs 中的组织方式
把视角拉回文档仓库本身,openmp.md 的组织方式体现了该项目的两条核心规范(见 CONTRIBUTING.md):
- Frontmatter 驱动站点生成:
category: tool决定其归类,filename: learnopenMP.cpp决定正文代码如何被合并导出为可下载的单一文件,contributors用于署名。这些字段会被 lint/frontmatter.py 的process_files()逐一校验(YAML 格式 + 白名单键名 + 值类型)。 - 风格约定:行宽尽量控制在 80 字符内、"示例优先于论述"(Prefer example to exposition)、面向有经验的程序员保持简洁——这正是本文各章节大量采用代码块 + 简短批注形式的原因。
- 站点构建:项目文档在 README.md 与 CONTRIBUTING.md 中说明了本地构建方式——克隆站点仓库与本仓库、
pip install -r requirements.txt、python build.py,再用python -m http.server预览;本仓库只保存 Markdown 源文档与 lint 工具(见 lint/ 目录,含 lint/encoding.sh、lint/requirements.txt)。
原文档末尾还列出了若干外部参考资源(并行编程入门讲义、OpenMP 官方教程合集、OpenMP 官方速查卡)。受限于本文不外链的约束,这里不再列出,感兴趣的读者可对照 openmp.md 末尾的 "Resources" 一节自行查阅。
总结
沿着 openmp.md 的脉络,本文完成了从入门到实战的完整闭环:
- 模型:Master 负责初始化,Slave 执行并行区域,线程 ID 由
omp_get_thread_num()获取; - 工具链:GCC 用
-fopenmp、Intel 用-openmp、pgcc 用-mp开启支持,export OMP_NUM_THREADS=N控制线程数; - API:
omp_get_max_threads/omp_get_num_threads/omp_num_procs/omp_in_parallel/omp_set_num_threads/omp_set_dynamic覆盖查询与控制; - 变量语义:默认共享(循环迭代计数器除外),显式
private/shared子句消除歧义,共享变量需防竞态; - 同步:
critical(互斥)、single(单线程执行)、atomic(原子更新)、ordered(有序执行)、barrier(屏障)、nowait(免等待)、reduction(归约)各司其职; - 实战:可并行化循环的判定标准(无跨迭代数据依赖)、加速比实测的方法与缓存性能带来的反直觉结论、Mandelbrot 示例中
schedule(dynamic)对负载不均的应对。
这些内容既可以直接作为日常开发的 OpenMP 速查手册,也为深入理解共享内存并行的同步与调度机制提供了起点。
- 文档
- 教程
【免费下载链接】learnxinyminutes-docs
Code documentation written as code! How novel and totally my idea!
相关推荐
从Python到Rust:Learn X in Y Minutes教程风格解析
从Python到Rust:Learn X in Y Minutes教程风格解析 本文深入分析了Learn X in Y Minutes项目中不同编程语言教程的教
文档教程几分钟看懂 jQuery:选择器、事件、效果与 DOM 操作(Learn X in Y Minutes 风格解析)
几分钟看懂 jQuery:选择器、事件、效果与 DOM 操作(Learn X in Y Minutes 风格解析) 本文基于 Learn X in Y Minu
文档教程Learn X in Y minutes:意大利语版 C++ 入门教程全解析
Learn X in Y minutes:意大利语版 C++ 入门教程全解析 本文基于本仓库 it/c++.md https://link.gitcode.co
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考