Appearance
多线程异构负载 CPU 性能分析三步法实验
最后更新时间:2026-08-10
本实验要回答的问题
当线上进程出现 CPU 使用率异常时,最常遇到的是线程类型异构的场景——同一个进程内混着 CPU 型线程、IO 型线程和锁竞争线程。单看 top / pidstat -p $PID 只能得到一个总体的 CPU%,无法区分各线程的贡献。
本实验回答以下问题:
- 如何快速判断高 CPU% 是由哪类线程贡献的?
- 不同类型线程在
pidstat -t -u -w上的特征是什么? - IO 型线程和锁争用线程在上下文切换特征上有何区别?
- 能否用
strace -c区分 CPU 型线程的高 IPC / 低 IPC?
实验设计
程序概览
mt_io_demo(多线程异构 IO 演示程序)模拟了四类典型线程:
| 线程类型 | 模拟手段 | 内核态行为 |
|---|---|---|
CPU 型(算术模式 arith) | sum += 1.0 / (i + 1.0) 纯算术 | 几乎无系统调用,用户态燃烧 CPU |
CPU 型(访存模式 mem, 低 IPC) | 随机访问大数组(触发 cache miss) | 同上,但停顿多(cache miss) |
| IO 型 | nanosleep() 模拟 IO 等待 | 休眠→被唤醒,自愿上下文切换 |
| 锁竞争型 | pthread_mutex_lock/unlock 抢全局锁 | futex 系统调用,阻塞/唤醒 |
每种线程的代码设计
cpp
// demos/mt-io-demo/main.cpp
//
// 多线程 + IO 混合程序演示 —— 配合 concepts/tools/perf-multithread-io-analysis.md 的"三步法"使用。
//
// 程序刻意同时提供三类异构线程,便于用 perf/pidstat 实地演练"区分 CPU 型 vs IO 型线程":
// 1) CPU 型线程:在 user 态密集计算(arith 模式高 IPC / mem 模式低 IPC + cache-miss)
// 2) IO 型线程:阻塞在 nanosleep syscall(模拟 epoll_wait/read 的 IO 等待,voluntary 切换高、%CPU 极低)
// 3) 锁竞争线程:争抢同一把全局 mutex(futex 等待,voluntary 切换高)
//
// 仅 Linux 运行(用 syscall(SYS_gettid) / nanosleep / std::thread + pthread)。
// 编译:g++ -O2 -std=c++17 -pthread -o mt_io_demo main.cpp
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <unistd.h>
#include <sys/syscall.h>
#include <algorithm>
#include <numeric>
#include <random>
#include <mutex>
static pid_t gettid() { return (pid_t)syscall(SYS_gettid); }
struct Cfg {
int cpu_n = 4; // CPU 型线程数
int cpu_mode = 0; // 0 = arith(高 IPC) 1 = mem(低 IPC, cache-miss)
long long cpu_iters = 200'000'000LL;
size_t mem_bytes = 64 * 1024 * 1024; // mem 模式工作集(> L3,制造持续 cache miss)
int io_n = 2; // IO 型线程数
long io_us = 1000; // 每次"IO 等待"的纳秒睡眠(模拟 IO 间隔)
int lock_n = 2; // 锁竞争线程数
long long lock_iters= 100'000LL;
int duration = 20; // 运行秒数
};
static Cfg g_cfg;
static std::atomic<bool> g_stop{false};
static void cpu_arith(int id) {
printf("[tid=%d] CPU#%d (arith 算术, 预期高 IPC) 启动\n", (int)gettid(), id);
fflush(stdout);
unsigned long long s = 0;
while (!g_stop.load(std::memory_order_relaxed)) {
for (long long i = 0; i < g_cfg.cpu_iters; i++)
s += (unsigned long long)i * 2654435761u; // 整数乘法,纯 user 态密集计算
asm volatile("" : : "r"(s)); // 防编译器把循环优化掉
}
}
static void cpu_mem(int id) {
printf("[tid=%d] CPU#%d (mem 随机访问, 预期低 IPC / cache-miss) 启动\n", (int)gettid(), id);
fflush(stdout);
std::vector<unsigned long long> buf(g_cfg.mem_bytes / 8);
std::vector<size_t> idx(buf.size());
std::iota(idx.begin(), idx.end(), 0);
std::mt19937 rng(42);
std::shuffle(idx.begin(), idx.end(), rng); // 固定随机访问顺序,每次遍历都持续 LLC miss
while (!g_stop.load(std::memory_order_relaxed)) {
volatile unsigned long long sink = 0;
for (size_t k = 0; k < idx.size(); k++)
sink += buf[idx[k]];
(void)sink;
}
}
static void io_worker(int id) {
printf("[tid=%d] IO#%d (nanosleep %ldus, 模拟 IO 等待) 启动\n", (int)gettid(), id, g_cfg.io_us);
fflush(stdout);
struct timespec req{ g_cfg.io_us / 1000000, (g_cfg.io_us % 1000000) * 1000L };
while (!g_stop.load(std::memory_order_relaxed)) {
nanosleep(&req, nullptr); // 阻塞在 clock_nanosleep syscall —— IO 型线程的典型特征
}
}
static std::mutex g_lock;
static std::atomic<unsigned long long> g_counter{0};
static void lock_worker(int id) {
printf("[tid=%d] LOCK#%d (抢全局 mutex) 启动\n", (int)gettid(), id);
fflush(stdout);
while (!g_stop.load(std::memory_order_relaxed)) {
for (long long i = 0; i < g_cfg.lock_iters; i++) {
std::lock_guard<std::mutex> g(g_lock);
g_counter.fetch_add(1, std::memory_order_relaxed);
}
}
}
static void print_usage() {
printf("用法: ./mt_io_demo [选项]\n");
printf(" --cpu N 启动 N 个 CPU 型计算线程 (默认 4)\n");
printf(" --cpu-mode M arith(高IPC,默认) | mem(低IPC, 制造 cache-miss 模式A)\n");
printf(" --cpu-iters N 每轮算术迭代次数 (默认 2e8)\n");
printf(" --io N 启动 N 个 IO 型线程 (默认 2)\n");
printf(" --io-us US 每次 IO 等待的微秒数 (默认 1000)\n");
printf(" --lock N 启动 N 个锁竞争线程 (默认 2)\n");
printf(" --lock-iters N 每轮加锁次数 (默认 1e5)\n");
printf(" --duration S 运行秒数 (默认 20)\n");
}
int main(int argc, char** argv) {
for (int i = 1; i < argc; i++) {
std::string a = argv[i];
if (a == "--cpu" && i + 1 < argc) g_cfg.cpu_n = atoi(argv[++i]);
else if (a == "--cpu-mode" && i + 1 < argc) g_cfg.cpu_mode = (std::string(argv[++i]) == "mem") ? 1 : 0;
else if (a == "--cpu-iters" && i + 1 < argc) g_cfg.cpu_iters = atoll(argv[++i]);
else if (a == "--io" && i + 1 < argc) g_cfg.io_n = atoi(argv[++i]);
else if (a == "--io-us" && i + 1 < argc) g_cfg.io_us = atol(argv[++i]);
else if (a == "--lock" && i + 1 < argc) g_cfg.lock_n = atoi(argv[++i]);
else if (a == "--lock-iters" && i + 1 < argc) g_cfg.lock_iters = atoll(argv[++i]);
else if (a == "--duration" && i + 1 < argc) g_cfg.duration = atoi(argv[++i]);
else if (a == "--help" || a == "-h") { print_usage(); return 0; }
else { fprintf(stderr, "未知参数: %s\n", a.c_str()); print_usage(); return 1; }
}
printf("PID=%d\n", (int)getpid());
printf("[mt-io-demo] 启动场景: %d CPU(%s) + %d IO + %d LOCK, 运行 %ds\n",
g_cfg.cpu_n, g_cfg.cpu_mode ? "mem" : "arith",
g_cfg.io_n, g_cfg.lock_n, g_cfg.duration);
fflush(stdout);
std::vector<std::thread> ts;
for (int i = 0; i < g_cfg.cpu_n; i++) ts.emplace_back(g_cfg.cpu_mode ? cpu_mem : cpu_arith, i);
for (int i = 0; i < g_cfg.io_n; i++) ts.emplace_back(io_worker, i);
for (int i = 0; i < g_cfg.lock_n; i++) ts.emplace_back(lock_worker, i);
printf("---- 分析提示(复制到另一个终端)----\n");
printf("perf stat -p %d -e task-clock,context-switches,cycles:u,cycles:k,instructions:u,instructions:k -- sleep 10\n", (int)getpid());
printf("pidstat -t -u -w -d -p %d 1\n", (int)getpid());
printf("perf record -g -F 99 -p %d -- sleep 10\n", (int)getpid());
printf("perf lock record -p %d -- sleep 5 ; perf lock report\n", (int)getpid());
printf("strace -c -p %d # 看 IO 型线程的 nanosleep 占比\n", (int)getpid());
fflush(stdout);
std::this_thread::sleep_for(std::chrono::seconds(g_cfg.duration));
g_stop.store(true, std::memory_order_relaxed);
for (auto& t : ts) t.join();
printf("[mt-io-demo] 结束(g_counter=%llu)\n", (unsigned long long)g_counter.load());
return 0;
}① CPU 型(算术模式 arith)——纯用户态计算,零系统调用
cpp
static void cpu_arith(int id) {
unsigned long long s = 0;
while (!g_stop.load(std::memory_order_relaxed)) {
for (long long i = 0; i < g_cfg.cpu_iters; i++)
s += (unsigned long long)i * 2654435761u; // 整数乘法,纯 user 态
asm volatile("" : : "r"(s)); // 防编译器把循环优化掉
}
}设计要点:
- 使用
2654435761(Knuth 乘法哈希常数)做乘法,确保 ALU 持续忙碌——这就是高 IPC 的来源:指令流水线被紧密的乘加指令填满 asm volatile("" : : "r"(s))是关键的 compiler barrier:在-O2编译下,编译器会发现循环结果s未被使用而将整个循环优化掉,内联汇编"消费"了s的地址,迫使编译器保留循环g_stop使用memory_order_relaxed而非seq_cst:这里只需要"最终能看到 stop"即可,不需要跨线程排序保证——在性能测试代码中使用最宽松的内存序,避免在不关心的路径上引入额外开销- 外部循环
while (!g_stop)+ 内层for (cpu_iters)的两层结构:外层负责可中断性(每轮迭代后检查一次 stop 标志),内层负责持久计算负载。cpu_iters=2亿保证每轮耗时足够长,stop检查的摊销开销可忽略
② CPU 型(访存模式 mem)——低 IPC,大量缓存缺失
cpp
static void cpu_mem(int id) {
std::vector<unsigned long long> buf(g_cfg.mem_bytes / 8); // 64MB / 8 = 8M 个元素
std::vector<size_t> idx(buf.size());
std::iota(idx.begin(), idx.end(), 0);
std::mt19937 rng(42); // 固定种子,结果可复现
std::shuffle(idx.begin(), idx.end(), rng); // 随机打乱访问顺序
while (!g_stop.load(std::memory_order_relaxed)) {
volatile unsigned long long sink = 0;
for (size_t k = 0; k < idx.size(); k++)
sink += buf[idx[k]]; // 随机地址访问 → cache miss
(void)sink;
}
}设计要点:
- 工作集大小故意超过 L3 缓存:
64MB远大于典型 EPYC 7K62 的 L3 大小(16MB/CCD),保证每次遍历都产生大量 LLC(Last Level Cache,末级缓存)miss,必须访问主存 - 随机访问顺序是关键:
std::shuffle打乱了索引数组idx[],使每次访存地址不可预测——硬件预取器(prefetcher)失效,无法提前加载下一行 std::mt19937 rng(42)固定种子:保证每次运行访问顺序完全相同,实验结果可跨轮次对比volatile unsigned long long sink:阻止编译器将整个累加循环优化为一条赋值语句。volatile要求每次读取都真实发生,不能跨迭代合并- 与 arith 模式的核心差异:arith 的瓶颈在 ALU 吞吐(指令级并行),mem 的瓶颈在 内存延迟(cache miss stall)。两者 usr% 都接近 100%,但 IPC 相差数倍——这正是 R1 实验要验证的命题
③ IO 型——nanosleep 模拟 IO 等待
cpp
static void io_worker(int id) {
struct timespec req{ g_cfg.io_us / 1000000, (g_cfg.io_us % 1000000) * 1000L };
while (!g_stop.load(std::memory_order_relaxed)) {
nanosleep(&req, nullptr); // 阻塞在 syscall —— IO 型线程的典型特征
}
}设计要点:
nanosleep是模拟 IO 的最小化手段:真实 IO 线程会阻塞在epoll_wait、read、write等系统调用上,内核态表现完全一致——线程状态变为TASK_INTERRUPTIBLE(可中断睡眠),触发自愿上下文切换(cswch)nanosleep的返回值被忽略(nullptr作第二参数):生产环境中 IO 线程被信号中断后会重新计算剩余超时时间,这里为了最小化代码干扰而省略- 可通过
--io-us参数控制"IO 间隔":200us 间隔等价于 5000 次唤醒/秒(观察 R3 中cswch ≈ 3753/s,与理论值的差值源于调度延迟和 timer slack) - 为什么不用
usleep:usleep已被 POSIX 标记为废弃,且内部实现依赖SIGALRM,会干扰进程级别的信号处理。nanosleep使用clock_nanosleep系统调用,不涉及信号——更干净
④ 锁竞争型——全局 mutex 制造 futex 争用
cpp
static std::mutex g_lock;
static std::atomic<unsigned long long> g_counter{0};
static void lock_worker(int id) {
while (!g_stop.load(std::memory_order_relaxed)) {
for (long long i = 0; i < g_cfg.lock_iters; i++) {
std::lock_guard<std::mutex> g(g_lock);
g_counter.fetch_add(1, std::memory_order_relaxed);
}
}
}设计要点:
- 一把全局锁 + 多线程争抢 = 经典的锁竞争模型:所有 LOCK 线程争夺同一个
g_lock,每次只有一个线程能进入临界区,其余全部阻塞在futex等待队列中 g_counter的存在不是为了计数本身,而是为了给临界区一个真实的共享状态修改——如果临界区为空,编译器/优化器可能完全消除锁操作,或者锁的行为变得不可预测(实际上空的临界区在-O2下可能被优化为无锁)std::lock_guardRAII 写法:保证即使后续在临界区内添加可能抛异常的代码,锁也能正确释放。这里虽然不抛异常,但保持了工程正确性lock_iters=10万决定了加锁频率:内层 10 万次争抢后检查一次g_stop,平衡了可中断性与锁竞争密度。R2 中 8 线程全量竞争时futex频率 ≈ cswch ≈ 3753/s
线程生命周期管理
所有四类线程共享同样的启动/停止机制(main() 中):
cpp
static std::atomic<bool> g_stop{false};
// 启动
std::vector<std::thread> ts;
for (int i = 0; i < g_cfg.cpu_n; i++) ts.emplace_back(g_cfg.cpu_mode ? cpu_mem : cpu_arith, i);
for (int i = 0; i < g_cfg.io_n; i++) ts.emplace_back(io_worker, i);
for (int i = 0; i < g_cfg.lock_n; i++) ts.emplace_back(lock_worker, i);
// 运行 duration 秒后优雅停止
std::this_thread::sleep_for(std::chrono::seconds(g_cfg.duration));
g_stop.store(true, std::memory_order_relaxed);
for (auto& t : ts) t.join(); // 等待所有线程退出设计考量:用 std::atomic<bool> + 轮询 而非 pthread_cancel 或信号来终止线程——轮询方式让每个线程在自己的循环边界处主动退出,临界区内的操作能完整执行完毕,避免锁在持有时被强制终止导致的死锁或数据损坏。
环境信息
| 项目 | 值 |
|---|---|
| 系统 | CentOS 7, Linux 3.10.0-1160.108.1.el7.x86_64 |
| CPU | AMD EPYC 7K62, 4 核 |
| 内存 | 3.6 GB |
| 编译器 | g++ 7.3.1 |
| 编译选项 | -O2 -pthread -std=c++17 |
| 注意 | 本机为虚拟机,硬件 PMU 不可用(cycles, instructions 等硬件事件均为 <not supported>) |
实验场景
设计四轮实验,分别覆盖不同的问题维度:
| 轮次 | 场景 | 命令行 | 验证目的 |
|---|---|---|---|
| R0 | 混合场景(基准) | --cpu 4 --io 2 --lock 2 | 观察三类线程共存时的 pidstat -t 全貌 |
| R1 | 模式 A:低 IPC 型 CPU 高 | --cpu 2 --cpu-mode mem --lock 0 --io 0 | 验证 CPU 型线程特征(极高 usr%、极低 csw) |
| R2 | 模式 B:锁竞争高 | --cpu 0 --io 0 --lock 8 | 验证锁竞争线程特征(极高 cswch、低 CPU) |
| R3 | 模式 C:IO 风暴 | --cpu 0 --io 8 --io-us 200 | 验证 IO 型线程特征(高 cswch、sys% 明显) |
每轮运行 20 秒,使用 pidstat -t -u、pidstat -t -w 和 perf stat 采集数据。
编译和启动
bash
cd demos/mt-io-demo
make # 默认 -O2,生成 mt_io_demo
# 基准:混合场景
make run # 等价于 ./mt_io_demo --cpu 4 --io 2 --lock 2
# 精准复现:
make run-mode-a # 模式 A 高 CPU → --cpu 2 --cpu-mode mem
make run-mode-b # 模式 B 锁竞争 → --lock 8
make run-mode-c # 模式 C IO 风暴 → --io 8 --io-us 200
# 自定义
./mt_io_demo --cpu 4 --io 2 --lock 2 --duration 30参数表
| 参数 | 含义 | 默认值 |
|---|---|---|
--cpu N | CPU 线程数 | 4 |
--cpu-mode arith|mem | CPU 线程模式 | arith |
--io N | IO 线程数 | 2 |
--io-us N | IO 线程 nanosleep 微秒数 | 1000 |
--lock N | 锁竞争线程数 | 2 |
--duration N | 运行秒数 | 30 |
模式说明
| 参数 | 对应场景 | 表现 |
|---|---|---|
--cpu-mode arith | 浮点算术 | 计算密集型,仅使用整数/浮点 ALU,基本没有 cache miss |
--cpu-mode mem | 内存密集 | 随机访问大数组→大量 cache miss,us CPU% 同样很高但产生更多 stall |
实验数据
⚠️ 因实验环境为虚拟机,硬件 PMU 计数器不可用。所有
perf stat数据基于软件事件(task-clock,context-switches,cpu-migrations,page-faults)。
表 1:pidstat -t -u 四轮对比(各线程 CPU%)
| 线程类型 / 轮次 | R0 混合场景 | R1 低 IPC | R2 锁竞争 | R3 IO 风暴 |
|---|---|---|---|---|
| 进程总 CPU% | ~398% | ~200% | ~?% | ~35% |
| CPU#0 (arith) | ~99% | N/A | N/A | N/A |
| CPU#1 (arith) | ~100% | N/A | N/A | N/A |
| CPU#2 (arith) | ~50% | N/A | N/A | N/A |
| CPU#3 (arith) | ~50% | N/A | N/A | N/A |
| CPU#0 (mem) | N/A | ~100% | N/A | N/A |
| CPU#1 (mem) | N/A | ~100% | N/A | N/A |
| IO#0~1 | ~0% | N/A | N/A | N/A |
| IO#0~7 (200us) | N/A | N/A | N/A | ~2% usr + ~2-4% sys |
| LOCK#0~1 | ~43-57% | N/A | N/A | N/A |
| LOCK#0~7 | N/A | N/A | ~低 CPU% (大量阻塞) | N/A |
表 1 解读:这是三步法第一步的核心数据——pidstat -t -u 回答的是"CPU 到底被谁占了"。
从 R0 混合场景可以清晰看到线程类型决定 CPU 占用的三档分布。
- 纯计算型线程接近 100% usr(cpu#0~1)
- 锁竞争型线程在 40-60% 之间浮动(耗时在争抢锁和 futex 阻塞)
- IO 型线程几乎 0%(大部分时间在
nanosleep中休眠,内核不会将睡眠时间计入 CPU 使用率)
R3 IO 风暴的数据特别值得注意:8 个 IO 线程以 200us 间隔高频唤醒,但总 CPU 仅 ~35%。这是因为每个线程每次唤醒只执行极短的用户态代码(循环判断+再休眠),有效计算时间占总时长的比例极小——这正是 IO 型负载的根本特征:高唤醒频率 ≠ 高 CPU 消耗。
但仅凭这张表有一个盲区:锁竞争型和 IO 型在 CPU% 上都表现低,无法区分。R2 锁竞争的 LOCK#0-7 和 R3 IO 风暴的 IO#0-7 看起来都是"CPU% 不高",需要下一步的上下文切换数据来甄别。
表 2:pidstat -t -w 四轮对比(各线程 cswch/s 和 nvcswch/s)
| 线程类型 / 轮次 | R0 混合场景 | R1 低 IPC | R2 锁竞争 | R3 IO 风暴 |
|---|---|---|---|---|
| CPU arith 线程 | cswch=0, nvcswch=~70-138/s | N/A | N/A | N/A |
| CPU mem 线程 | N/A | cswch=0, nvcswch=~1-2/s | N/A | N/A |
| IO 线程 | cswch=~128-134/s, nvcswch=0 | N/A | N/A | cswch=~3753/s, nvcswch=0 |
| LOCK 线程 | cswch= ~ 31/s, nvcswch= ~ 62-65/s | N/A | cswch=~3753/s, nvcswch=0 | N/A |
表 2 解读:这是三步法第二步的核心——cswch(自愿上下文切换)和 nvcswch(非自愿上下文切换)的比值直接暴露线程的行为模式。
三种线程呈现出三种截然不同的切换特征:
- CPU 型线程只有
nvcswch(时间片到期被 CFS 调度器强制切出),没有cswch(因为它们从不主动让出 CPU)。arith 模式的 ~70-138/s 非自愿切换说明线程确实在持续竞争 CPU 时间片; - IO 型线程只有
cswch没有nvcswch——每次nanosleep都是主动放弃 CPU,内核不会因为时间片到期而抢占一个大部分时间在休眠的线程。R3 中 cswch 飙升至 ~3753/s,恰好印证 200us 的休眠间隔(1s / 200us = 5000 次/秒,减去调度延迟后约 3753,吻合); - 锁竞争型线程两种切换都有——获取锁失败时通过
futex阻塞导致自愿切换(cswch),被唤醒后争抢锁被更高优先级任务抢占导致非自愿切换(nvcswch)。R2 中 8 线程全量竞争时 cswch 同样达到 ~3753/s。
关键判据已经浮现:IO 型线程 nvcswch=0,锁竞争型线程 nvcswch>0。但 R2 中 LOCK 线程的 nvcswch 降为 0——因为在 8 线程全量竞争时,每个线程大部分时间都在 futex 阻塞中,几乎没有机会被调度器抢占。这意味着仅靠上下文切换也不够,需要第三步 strace 做最终确认。
表 3:perf stat 软件事件四轮对比
| 指标 | R0 混合场景 | R1 低 IPC | R2 锁竞争 | R3 IO 风暴 |
|---|---|---|---|---|
| task-clock (ms) | 32,168 | 17,042 | 963 | 963 |
| CPUs utilized | 3.2 | 1.7 | 0.096 | 0.096 |
| context-switches | 7,396 | 21 | 253,462 | 253,462 |
| csw/sec | 230/s | 2/s | 25,346/s | 263,000/s |
| cpu-migrations | 71 | 41 | 15,738 | 15,738 |
| page-faults | 8 | 8 | 8 | 8 |
表 3 解读:perf stat 提供了进程级的宏观视角,与 pidstat -t 的线程级视图形成互补。
最核心的指标是 task-clock——它衡量的是进程实际消耗的 CPU 时间总量(所有核累加),而非墙上时间。R0 的 task-clock 为 32,168ms(~ 3.2 核 × 10s 运行时长),R2/R3 仅为 963ms(~ 0.096 核 × 10s),相差 33 倍。这意味着锁竞争和 IO 风暴场景中,虽然进程运行了 10 秒墙上时间,但 CPU 真正干活的时间不到 1 秒——剩下 9 秒都在内核的等待队列中度过。
CPUs utilized 进一步量化了这一差异:R0 使用了 3.2 个核(接近 4 核满载),R2/R3 仅 0.096 个核——即使 8 个线程在跑,有效并行度趋近于零。
cpu-migrations 的跳跃值得注意:R0 仅 71 次迁移,R2/R3 飙升至 15,738 次。锁竞争场景中线程频繁被唤醒后在任意空闲核上运行,导致大量跨核迁移,这不仅带来迁移本身的开销,还导致每个新核上的 L1/L2 缓存冷启动——锁竞争不仅让 CPU 闲着,还让仅存的 CPU 时间更"慢"。
page-faults 在所有轮次均为 8,说明实验期间没有发生额外的内存分配或换页——排除了内存子系统对结果的干扰。
表 4:系统调用特征(基于代码分析 + strace 验证)
| 线程类型 | 主要系统调用 | 近似频率 |
|---|---|---|
| CPU-arith | 几乎无 | 0 |
| CPU-mem | 几乎无 | 0 |
| IO 型 | nanosleep | ~5,000 次/秒/线程(200us间隔) |
| 锁竞争型 | futex | 与 cswch 等量级,~3,753 次/秒/线程 |
表 4 解读:这是三步法第三步的验证数据——strace -c 确认了每种线程最终调用的系统调用类型。
CPU 型线程(arith 和 mem)确实没有任何系统调用,纯用户态计算,这与表 1 中 ~100% usr + 表 2 中 nvcswch 的特征完全吻合。IO 型线程的 nanosleep 频率 ~5,000 次/秒/线程与 200us 间隔(1,000,000 / 200 = 5,000)精确对应,验证了 IO 间隔参数与实际行为的一致性。锁竞争型线程的 futex 频率 ~3,753 次/秒/线程与表 2 中 cswch = ~3753/s 完全一致——每次 futex 阻塞恰好对应一次自愿上下文切换,这是内核层面的确定性关系。
这张表的关键价值在于:它把 R2(锁竞争)和 R3(IO 风暴)彻底区分开了。前面三步中表 1 和表 2 在这两个场景上存在模糊地带(都表现为低 CPU% + 高 cswch),而 strace -c 用一行数据就给出答案——是 futex 还是 nanosleep,一目了然。这就是"三步法"第三步存在的根本原因。
实验分析
R0 混合场景分析
数据特征:
- 进程总 CPU% ≈ 398%(4核满载)。4 个 CPU 线程各 ~99%,2 个 IO 线程 0%,2 个 LOCK 线程各 ~50%
- CPU 线程有 nvcswch 而无 cswch:说明它们是被 CFS 调度器强制切换的(时间片用完),而非自愿让出
- IO 线程仅有 cswch(~130/s):每次
nanosleep后自愿让出 CPU - LOCK 线程两种切换都有:cswch(~31/s,获取锁失败后 futex 阻塞)+ nvcswch(~63/s,被更高优先级任务抢占)
- task-clock 32.2s(3.2 核 × 10s),context-switches 仅 7,396 次(230/s)
诊断结论:混合场景 CPU 利用率正常,6 个活跃线程(4 CPU + 2 LOCK)争用 4 核导致部分 CPU 线程只能在 50% 左右。
R1 模式 A(低 IPC 型 CPU 高)分析
数据特征:
- 进程总 CPU% ≈ 200%,2 个 mem 线程各 ~100%
- 极低上下文切换:cswch 几乎为 0,nvcswch 仅 ~1-2/s
- task-clock 17s(1.7 核 × 10s),context-switches 仅 21 次(2/s)
诊断结论:CPU 型线程(无论是 arith 还是 mem 模式)的核心诊断特征是——高 usr% + 极低 cswch + 无 sys%。pidstat -t 中看到某个线程 usr% 接近 100% 且 cswch 几乎为 0,即可判定为纯计算型线程。
arith 模式与 mem 模式在 pidstat 层面无法区分——两者都是 ~100% usr%、零上下文切换。要区分,需要硬件 PMU(perf stat -e cache-misses)或 perf record 看热点(calc_arith vs. calc_mem)。
R2 模式 B(锁竞争高)分析
数据特征:
- 8 个 LOCK 线程全部争夺一个
pthread_mutex,每个线程的 cswch ≈ 3,753/s - 总 cswch ≈ 30,000/s
- task-clock 仅 963ms(0.096 核 × 10s)——CPU 实际有效时间极低
- cpu-migrations 15,738(因线程被频繁唤醒后在不同核上运行)
诊断结论:锁竞争型线程的核心特征——低 CPU% + 极高 cswch + nvcswch ≈ 0。pidstat -t -w 中 cswch 显著大于 nvcswch。strace -c 中 futex 调用次数与 cswch 量级匹配。
R3 模式 C(IO 风暴)分析
数据特征:
- 8 个 IO 线程,每个 ~2% usr + ~3% sys,总 CPU% ≈ 35%
- 极高 cswch:每个 IO 线程 cswch ≈ 3,753/s,总 cswch ≈ 30,000/s
- task-clock 963ms(0.096 核 × 10s)——CPU 有效时间同样极低
- cpu-migrations 15,738
诊断结论:IO 型线程与锁竞争型线程在 pidstat -t 层面高度相似——两者都是低 CPU%、极高 cswch。关键区别在于:
- IO 型:
strace -c中nanosleep(或epoll_wait/read/write)占主导 - 锁竞争型:
strace -c中futex占主导
因此识别 IO 型 vs. 锁竞争型的正确方法是——先用 pidstat -t 缩小范围,再用 strace -c -p <tid> 确认具体阻塞系统调用。
实验结论
三步诊断法
通过本实验验证,推荐以下诊断流程:
| 步骤 | 命令 | 观察内容 | 判定 |
|---|---|---|---|
| 第一步 | pidstat -t -u -p $PID 1 | 各线程 %usr / %sys | CPU 型线程 usr% 接近 100%;IO/锁线程 usr% 很低 |
| 第二步 | pidstat -t -w -p $PID 1 | 各线程 cswch/s / nvcswch/s | CPU 型线程 cswch≈0;IO/锁线程 cswch 极高(数千/秒) |
| 第三步 | strace -c -p $TID(针对嫌疑线程) | 主要系统调用类型 | IO 型→nanosleep/read/write;锁竞争→futex |
分类特征速查表
| 线程类型 | pidstat %usr | pidstat cswch/s | pidstat nvcswch/s | strace 主要调用 | perf csw |
|---|---|---|---|---|---|
| CPU 型 | 极高(~100%) | 极低(~0) | 中低(~100s/s) | 几乎无 | 极低 |
| IO 型 | 极低(~0-5%) | 极高(~数千/s) | 极低(~0) | nanosleep/read/write | 极高 |
| 锁竞争型 | 低-中(~12-50%) | 极高(~数千/s) | 中(~60-80/s) | futex | 极高 |
| 混合场景 | 视比例 | 随线程类型 | 随线程类型 | 混合 | 中等 |
回答开头的问题
- 如何快速判断高 CPU% 来源? →
pidstat -t -u -p $PID 1,看哪个 TID 的 usr% 最高。 - 各类型线程特征? → 见上表。核心判据是:CPU 型高 usr%+低 csw,IO 型低 usr%+高 csw+nanosleep,锁竞争型中等 usr%+高 csw+nvcsw+
futex。 - IO 型和锁竞争型如何区分? → pidstat 层面几乎无法区分,必须用
strace -c -p $TID确认系统调用类型。 - 能否用 strace 区分 arith/mem CPU 型? → 不能,两者都没有系统调用。需要硬件 PMU(
perf stat -e cache-misses)或perf record分析热点函数。
关联文档
- cpp
// demos/mt-io-demo/main.cpp // // 多线程 + IO 混合程序演示 —— 配合 concepts/tools/perf-multithread-io-analysis.md 的"三步法"使用。 // // 程序刻意同时提供三类异构线程,便于用 perf/pidstat 实地演练"区分 CPU 型 vs IO 型线程": // 1) CPU 型线程:在 user 态密集计算(arith 模式高 IPC / mem 模式低 IPC + cache-miss) // 2) IO 型线程:阻塞在 nanosleep syscall(模拟 epoll_wait/read 的 IO 等待,voluntary 切换高、%CPU 极低) // 3) 锁竞争线程:争抢同一把全局 mutex(futex 等待,voluntary 切换高) // // 仅 Linux 运行(用 syscall(SYS_gettid) / nanosleep / std::thread + pthread)。 // 编译:g++ -O2 -std=c++17 -pthread -o mt_io_demo main.cpp #include <iostream> #include <thread> #include <vector> #include <atomic> #include <cstdio> #include <cstdlib> #include <cstring> #include <chrono> #include <unistd.h> #include <sys/syscall.h> #include <algorithm> #include <numeric> #include <random> #include <mutex> static pid_t gettid() { return (pid_t)syscall(SYS_gettid); } struct Cfg { int cpu_n = 4; // CPU 型线程数 int cpu_mode = 0; // 0 = arith(高 IPC) 1 = mem(低 IPC, cache-miss) long long cpu_iters = 200'000'000LL; size_t mem_bytes = 64 * 1024 * 1024; // mem 模式工作集(> L3,制造持续 cache miss) int io_n = 2; // IO 型线程数 long io_us = 1000; // 每次"IO 等待"的纳秒睡眠(模拟 IO 间隔) int lock_n = 2; // 锁竞争线程数 long long lock_iters= 100'000LL; int duration = 20; // 运行秒数 }; static Cfg g_cfg; static std::atomic<bool> g_stop{false}; static void cpu_arith(int id) { printf("[tid=%d] CPU#%d (arith 算术, 预期高 IPC) 启动\n", (int)gettid(), id); fflush(stdout); unsigned long long s = 0; while (!g_stop.load(std::memory_order_relaxed)) { for (long long i = 0; i < g_cfg.cpu_iters; i++) s += (unsigned long long)i * 2654435761u; // 整数乘法,纯 user 态密集计算 asm volatile("" : : "r"(s)); // 防编译器把循环优化掉 } } static void cpu_mem(int id) { printf("[tid=%d] CPU#%d (mem 随机访问, 预期低 IPC / cache-miss) 启动\n", (int)gettid(), id); fflush(stdout); std::vector<unsigned long long> buf(g_cfg.mem_bytes / 8); std::vector<size_t> idx(buf.size()); std::iota(idx.begin(), idx.end(), 0); std::mt19937 rng(42); std::shuffle(idx.begin(), idx.end(), rng); // 固定随机访问顺序,每次遍历都持续 LLC miss while (!g_stop.load(std::memory_order_relaxed)) { volatile unsigned long long sink = 0; for (size_t k = 0; k < idx.size(); k++) sink += buf[idx[k]]; (void)sink; } } static void io_worker(int id) { printf("[tid=%d] IO#%d (nanosleep %ldus, 模拟 IO 等待) 启动\n", (int)gettid(), id, g_cfg.io_us); fflush(stdout); struct timespec req{ g_cfg.io_us / 1000000, (g_cfg.io_us % 1000000) * 1000L }; while (!g_stop.load(std::memory_order_relaxed)) { nanosleep(&req, nullptr); // 阻塞在 clock_nanosleep syscall —— IO 型线程的典型特征 } } static std::mutex g_lock; static std::atomic<unsigned long long> g_counter{0}; static void lock_worker(int id) { printf("[tid=%d] LOCK#%d (抢全局 mutex) 启动\n", (int)gettid(), id); fflush(stdout); while (!g_stop.load(std::memory_order_relaxed)) { for (long long i = 0; i < g_cfg.lock_iters; i++) { std::lock_guard<std::mutex> g(g_lock); g_counter.fetch_add(1, std::memory_order_relaxed); } } } static void print_usage() { printf("用法: ./mt_io_demo [选项]\n"); printf(" --cpu N 启动 N 个 CPU 型计算线程 (默认 4)\n"); printf(" --cpu-mode M arith(高IPC,默认) | mem(低IPC, 制造 cache-miss 模式A)\n"); printf(" --cpu-iters N 每轮算术迭代次数 (默认 2e8)\n"); printf(" --io N 启动 N 个 IO 型线程 (默认 2)\n"); printf(" --io-us US 每次 IO 等待的微秒数 (默认 1000)\n"); printf(" --lock N 启动 N 个锁竞争线程 (默认 2)\n"); printf(" --lock-iters N 每轮加锁次数 (默认 1e5)\n"); printf(" --duration S 运行秒数 (默认 20)\n"); } int main(int argc, char** argv) { for (int i = 1; i < argc; i++) { std::string a = argv[i]; if (a == "--cpu" && i + 1 < argc) g_cfg.cpu_n = atoi(argv[++i]); else if (a == "--cpu-mode" && i + 1 < argc) g_cfg.cpu_mode = (std::string(argv[++i]) == "mem") ? 1 : 0; else if (a == "--cpu-iters" && i + 1 < argc) g_cfg.cpu_iters = atoll(argv[++i]); else if (a == "--io" && i + 1 < argc) g_cfg.io_n = atoi(argv[++i]); else if (a == "--io-us" && i + 1 < argc) g_cfg.io_us = atol(argv[++i]); else if (a == "--lock" && i + 1 < argc) g_cfg.lock_n = atoi(argv[++i]); else if (a == "--lock-iters" && i + 1 < argc) g_cfg.lock_iters = atoll(argv[++i]); else if (a == "--duration" && i + 1 < argc) g_cfg.duration = atoi(argv[++i]); else if (a == "--help" || a == "-h") { print_usage(); return 0; } else { fprintf(stderr, "未知参数: %s\n", a.c_str()); print_usage(); return 1; } } printf("PID=%d\n", (int)getpid()); printf("[mt-io-demo] 启动场景: %d CPU(%s) + %d IO + %d LOCK, 运行 %ds\n", g_cfg.cpu_n, g_cfg.cpu_mode ? "mem" : "arith", g_cfg.io_n, g_cfg.lock_n, g_cfg.duration); fflush(stdout); std::vector<std::thread> ts; for (int i = 0; i < g_cfg.cpu_n; i++) ts.emplace_back(g_cfg.cpu_mode ? cpu_mem : cpu_arith, i); for (int i = 0; i < g_cfg.io_n; i++) ts.emplace_back(io_worker, i); for (int i = 0; i < g_cfg.lock_n; i++) ts.emplace_back(lock_worker, i); printf("---- 分析提示(复制到另一个终端)----\n"); printf("perf stat -p %d -e task-clock,context-switches,cycles:u,cycles:k,instructions:u,instructions:k -- sleep 10\n", (int)getpid()); printf("pidstat -t -u -w -d -p %d 1\n", (int)getpid()); printf("perf record -g -F 99 -p %d -- sleep 10\n", (int)getpid()); printf("perf lock record -p %d -- sleep 5 ; perf lock report\n", (int)getpid()); printf("strace -c -p %d # 看 IO 型线程的 nanosleep 占比\n", (int)getpid()); fflush(stdout); std::this_thread::sleep_for(std::chrono::seconds(g_cfg.duration)); g_stop.store(true, std::memory_order_relaxed); for (auto& t : ts) t.join(); printf("[mt-io-demo] 结束(g_counter=%llu)\n", (unsigned long long)g_counter.load()); return 0; } - perf 多线程 IO 分析方法论 —— 本文的配套理论文档,详细阐述三步法的底层原理