本文总结了 OpenMP (libgomp) 和 OpenBLAS 两个库的线程管理机制,包括线程池创建、生命周期、环境变量控制以及它们之间的交互关系。
1. 核心概念区分
1.1 物理核心 vs 逻辑核心
- 物理核心 (Physical Cores): 实际的 CPU 计算单元
- 逻辑核心 (Logical Cores): 操作系统看到的核心数
- 启用超线程 (Hyper-Threading) 时:
- 逻辑核心 = 物理核心 × 2
- 例: 24 物理核心 + HT ➜ 48 逻辑核心
对 GEMM 等计算密集型任务的影响:
- 超线程的两个逻辑核心共享同一物理核心的 L1/L2 Cache 和 FMA 单元
- 使用 48 线程(逻辑核心数)可能比 24 线程(物理核心数)更慢
- 建议: GEMM 类任务使用物理核心数
1.2 并行 vs 并发
线程数 ≤ 核心数: 并行 (Parallel)
线程数 > 核心数: 并发 (Concurrent)
- 线程共享核心,时间片轮转
- 有上下文切换开销
- 性能可能下降
2. OpenMP (libgomp) 线程管理
2.1 线程模型:按需创建/销毁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| 【OpenMP 线程模型:动态 Fork-Join 视图】
时间轴 (↓) │ │ [ 串行区 ] (只有主线程) │ 主线程 T0 🟢 │ ▼ ┌────────────────── FORK (分叉) ──────────────────┐ │指令: #pragma omp parallel num_threads(24) │ │动作: 主线程创建/唤醒 23 个工作线程 │ └─────────────────────────────────────────────────┘ │ │ 并行区 1 (共24线程) ├─── T0 🟢 (主) ──→ [ 执行任务 ] ──┐ ├─── T1 🟠 (工) ──→ [ 执行任务 ] ──┤ ├─── T2 🟠 (工) ──→ [ 执行任务 ] ──┤ │ . . . (省略 T3-T22) . . . │ └─── T23🟠 (工) ──→ [ 执行任务 ] ──┘ │ ▼ ┌────────────────── JOIN (合并) ──────────────────┐ │动作: 遇到隐式屏障,等待所有线程完成。 │ │ 工作线程 T1-T23 被销毁 (概念上)。 │ └─────────────────────────────────────────────────┘ │ │ [ 串行区 ] (只有主线程) │ 主线程 T0 🟢 │ ▼ ┌────────────────── FORK (分叉) ──────────────────┐ │指令: #pragma omp parallel num_threads(4) │ │动作: 主线程创建/唤醒 3 个工作线程 │ └─────────────────────────────────────────────────┘ │ │ 并行区 2 (共4线程 - 宽度变窄) ├─── T0 🟢 (主) ──→ [ 执行任务 ] ──┐ ├─── T1 🟠 (工) ──→ [ 执行任务 ] ──┤ ├─── T2 🟠 (工) ──→ [ 执行任务 ] ──┤ └─── T3 🟠 (工) ──→ [ 执行任务 ] ──┘ │ ▼ ┌────────────────── JOIN (合并) ──────────────────┐ │动作: 屏障同步。工作线程 T1-T3 被销毁。 │ └─────────────────────────────────────────────────┘ │ │ [ 串行区 ] (只有主线程) │ 主线程 T0 🟢 │ ▼ ┌────────────────── FORK (分叉) ──────────────────┐ │指令: #pragma omp parallel num_threads(48) │ │动作: 主线程创建/唤醒 47 个工作线程 │ └─────────────────────────────────────────────────┘ │ │ 并行区 3 (共48线程 - 宽度显著增加) ├─── T0 🟢 (主) ───→ [ 执行任务 ] ──┐ │ │ ├─── [ 密集的工作线程组 T1 - T47 ] ────┤ 🟠 (×47) │ (此处代表大量并行执行的线程) │ │ │ └─── (示意图宽度不足以全画出) ─────────┘ │ ▼ ┌────────────────── JOIN (合并) ──────────────────┐ │动作: 屏障同步。工作线程 T1-T47 被销毁。 │ └─────────────────────────────────────────────────┘ │ │ [ 串行区 ] (只有主线程) │ 主线程 T0 🟢 │ ▼ [ 程序结束 ]
图例: 🟢 = 主线程 (Master Thread), 在整个程序生命周期内存在。 🟠 = 工作线程 (Worker Threads), 仅在并行区内存在 (逻辑上)。
|
注:libgomp 的实际实现可能有线程缓存优化,但观测到的行为是按需变化
2.2 关键特性
| 特性 | 说明 |
|---|
| 初始化时机 | 首次进入 parallel region |
| 线程数 | 每个 region 可以不同 |
| 生命周期 | region 结束后销毁(或缓存复用) |
| 默认线程数 | omp_get_max_threads() |
2.3 API 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int omp_get_max_threads(void);
void omp_set_num_threads(int num);
int omp_get_num_procs(void);
int omp_get_num_threads(void);
int omp_get_thread_limit(void);
|
2.4 线程数控制优先级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ┌────────────────────────────────────────────────────────────── │ OpenMP 线程设置优先级(从高到低 ⬇️) ├────────────────────────────────────────────────────────────── │ 🥇 1. #pragma omp parallel num_threads(N) │ ├── 最高优先级,覆盖一切 │ ├── 可以超过 omp_get_max_threads() │ └── 可以超过逻辑核心数(变成并发) │ │ 🥈 2. omp_set_num_threads(N) │ ├── 运行时修改默认值 │ └── 影响后续没有 num_threads 子句的 region │ │ 🥉 3. OMP_NUM_THREADS 环境变量 │ ├── 程序启动时的初始默认值 │ └── 设置 omp_get_max_threads() 的初始值 │ │ 🔹 4. 实现默认值 (System Default) │ └── 通常等于逻辑核心数 └──────────────────────────────────────────────────────────────
|
2.5 OMP_NUM_THREADS 不是硬上限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
omp_get_max_threads();
#pragma omp parallel { omp_get_num_threads(); }
#pragma omp parallel num_threads(24) { omp_get_num_threads(); }
#pragma omp parallel num_threads(64) { omp_get_num_threads(); }
|
3. OpenBLAS 线程管理
3.1 线程池模型:固定池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ┌────────────────────────────────────────────────────────────── │ 🚀 OpenBLAS 线程生命周期 (Thread Lifecycle) ├────────────────────────────────────────────────────────────── │ │ ▶️ 程序启动 (Program Start) │ │ │ ▼ │ 📦 动态链接器加载 libopenblas.so │ │ │ ▼ │ ╔══ ⚙️ 初始化阶段 (Constructor Phase) ════════════════════ │ ║ (在 main() 之前执行 __attribute__((constructor))) │ ║ │ ║ 1️⃣ 📖 读取环境变量 OPENBLAS_NUM_THREADS │ ║ 2️⃣ 🏊 创建初始大小的线程池 (Thread Pool Created) │ ║ 3️⃣ 📈 关键特性: 线程池可扩展,但不再缩小 (Grow Only) │ ╚══════════════════════════════════════════════════════════ │ │ │ ▼ │ 🧊 C++ 静态初始化 (Static Initializers) │ *(注: 此时 OpenBLAS 线程池已存在并就绪)* │ │ │ ▼ │ 🏁 main() 函数开始执行 │ └──────────────────────────────────────────────────────────────
|
3.2 关键特性
| 特性 | 说明 |
|---|
| 初始化时机 | 库加载时(constructor),在 main() 之前 |
| 触发条件 | 代码中引用了任何 OpenBLAS 符号(延迟绑定) |
| 初始池大小 | 由 OPENBLAS_NUM_THREADS 环境变量决定(默认 = 逻辑核心数) |
| 池大小变化 | 只扩展,不缩小(类似 std::vector 的 capacity) |
| 生命周期 | 程序结束时销毁 |
3.3 API 函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| int openblas_get_num_threads(void);
void openblas_set_num_threads(int num);
int openblas_get_num_procs(void);
char* openblas_get_config(void);
|
3.4 线程池行为详解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| ┌────────────────────────────────────────────────────────────────── │ 🚀 OPENBLAS_NUM_THREADS=8 启动程序 ├────────────────────────────────────────────────────────────────── │ 🟩 初始状态: │ 📦 线程池: [T0][T1]...[T7] ← (共 8 个线程) │ ⚙️ 使用数: 8 │ ├────────────────────────────────────────────────────────────────── │ ▶️ openblas_set_num_threads(24): 📈 请求数 > 池大小 → 扩展! │ │ 📦 线程池: [T0][T1]...[T7][T8]...[T23] ← (扩展到 24 个) │ ⚙️ 使用数: 24 │ ├────────────────────────────────────────────────────────────────── │ ▶️ openblas_set_num_threads(4): 📉 请求数 < 池大小 → 不缩小! │ │ 📦 线程池: [T0][T1][T2][T3][💤][💤]...[💤] (共 24 个仍存活) │ ↑ ↑ ↑ ↑ ↑ │ 🔥 工作中... 🛌 睡眠中 (20 个) │ ⚙️ 使用数: 4 │ ├────────────────────────────────────────────────────────────────── │ ▶️ openblas_set_num_threads(48): 📈 请求数 > 池大小 → 再次扩展! │ │ 📦 线程池: [T0][T1]...[T47] ← (扩展到 48 个) │ ⚙️ 使用数: 48 │ ├────────────────────────────────────────────────────────────────── │ ▶️ openblas_set_num_threads(4): 📉 请求数 < 池大小 → 不缩小! │ │ 📦 线程池: [T0][T1][T2][T3][💤]...[💤] (共 48 个仍存活) │ ↑ ↑ ↑ ↑ ↑ │ 🔥 工作中... 🛌 睡眠中 (44 个) │ ⚙️ 使用数: 4 │ └──────────────────────────────────────────────────────────────────
|
总结:
- OPENBLAS_NUM_THREADS 控制初始池大小
set_num_threads(N) 当 N > 池大小时,扩展池set_num_threads(N) 当 N < 池大小时,只改变使用数- 池只增不减,类似
std::vector 的 capacity
3.5 环境变量
| 环境变量 | 作用 | 优先级 |
|---|
OPENBLAS_NUM_THREADS | 控制初始线程池大小 | 最高 |
GOTO_NUM_THREADS | 兼容旧版 GotoBLAS | 次之 |
OMP_NUM_THREADS | Fallback(部分构建版本) | 较低 |
| (无) | 使用逻辑核心数 | 默认 |
注意: 环境变量只影响初始池大小,运行时 set_num_threads() 可以扩展池。
4. 对比总结
4.1 核心差异
| 维度 | OpenMP(libgomp) | OpenBLAS |
|---|
| 线程模型 | 按需创建 / 销毁 | 可扩展池(只增不减) |
| 初始化时机 | 首次 parallel region | 库加载时(main 之前) |
| 池大小可变 | ✅ 每次 region 可不同 | ✅ 可扩展,❌ 不缩小 |
| “池大小” vs “使用数” | 统一 | 分离(池 ≥ 使用数) |
| 空闲线程 | 销毁 | 睡眠,保持存活 |
| 控制方式 | 环境变量 + API + 子句 | 环境变量 + set_num_threads() |
4.2 get_num_procs() 的含义
两个库的 get_num_procs() 含义相同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ┌────────────────────────────────────────────────────────────────── │ 🖥️ 硬件核心数查询 (Hardware Query) │ omp_get_num_procs() / openblas_get_num_procs() ├────────────────────────────────────────────────────────────────── │ │ 🔢 核心功能: │ └── 返回系统的逻辑核心数 (Logical Core Count) │ │ ⚓️ 属性特征: │ └── 属于硬件物理信息,是固定值 (Fixed Value) │ │ 🛡️ 独立性: │ └── 坚决不受 OMP_NUM_THREADS 等环境变量影响 │ │ 🔗 C++ 等价物: │ └── ≈ std::thread::hardware_concurrency() │ └──────────────────────────────────────────────────────────────────
|
5. 环境变量交互
5.1 OpenBLAS 的 Fallback 机制
部分 OpenBLAS 构建版本在 OPENBLAS_NUM_THREADS 未设置时,会检查其他环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| ┌────────────────────────────────────────────────────────────────── │ 📉 OpenBLAS 环境变量优先级 (Priority Chain) │ (检查顺序:若上级未设置,则尝试下级) ├────────────────────────────────────────────────────────────────── │ │ 👑 1. OPENBLAS_NUM_THREADS │ │ │ ├── 最高优先级 (Highest Priority) │ └── 专门针对 OpenBLAS 的设置 │ │ │ ▼ │ 🏛️ 2. GOTO_NUM_THREADS │ │ │ ├── 历史兼容 (Legacy) │ └── 兼容古老的 GotoBLAS 设置 │ │ │ ▼ │ 🤝 3. OMP_NUM_THREADS │ │ │ ├── 通用回退 (Fallback) │ └── 仅在编译时开启 OpenMP 支持时生效 │ │ │ ▼ │ 💻 4. 硬件逻辑核心数 (System Default) │ │ ├── 兜底方案 (Baseline) │ └── 如果以上都没设置,占满所有核心 │ └──────────────────────────────────────────────────────────────────
|
5.2 推荐做法
为避免意外行为,显式设置两个环境变量:
1 2 3
| export OPENBLAS_NUM_THREADS=24 export OMP_NUM_THREADS=24 ./program
|
或单次运行:
1
| OPENBLAS_NUM_THREADS=24 OMP_NUM_THREADS=24 ./program
|