OpenMP / OpenBLAS 线程管理策略总结

本文最后更新于 2025年12月27日 晚上

本文总结了 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);

// 设置后续 parallel region 的默认线程数
void omp_set_num_threads(int num);

// 获取系统逻辑核心数(硬件信息,固定值)
int omp_get_num_procs(void);

// 在 parallel region 内:获取当前 region 的实际线程数
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_NUM_THREADS=8

omp_get_max_threads(); // → 8 (默认值)

#pragma omp parallel // 不指定 num_threads
{
omp_get_num_threads(); // → 8 (使用默认值)
}

#pragma omp parallel num_threads(24) // 显式指定
{
omp_get_num_threads(); // → 24 (可以超过默认值!)
}

#pragma omp parallel num_threads(64) // 超过逻辑核心数
{
omp_get_num_threads(); // → 64 (可以! 但是是并发不是并行)
}

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);

// 设置线程数:
// N > 当前池大小 → 扩展池
// N < 当前池大小 → 只改变使用数,池不缩小
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::vectorcapacity

3.5 环境变量

环境变量作用优先级
OPENBLAS_NUM_THREADS控制初始线程池大小最高
GOTO_NUM_THREADS兼容旧版 GotoBLAS次之
OMP_NUM_THREADSFallback(部分构建版本)较低
(无)使用逻辑核心数默认

注意: 环境变量只影响初始池大小,运行时 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

OpenMP / OpenBLAS 线程管理策略总结
https://codebearjourney.top/hpc/cpu/omp-blas-thread-management
作者
Tianyu Xiong
发布于
2025年12月27日
更新于
2025年12月27日
许可协议