简介:面向开发者、系统管理员与硬件爱好者的Windows CPU负载模拟工具,通过浏览器页面即可自定义CPU占用率与持续时间,用于系统压力测试、性能调优或稳定性验证。压缩包约34KB,仅含2个文件,包括一个HTML交互页面和一个JavaScript脚本,无需安装或配置环境,下载后打开即可使用,轻量便捷;脚本基于jQuery编写,结构清晰,便于学习或二次改造。使用时可在页面上设定目标占用率与时长,关闭页面即自动停止模拟,尤其适合在360极速浏览器等Chromium内核环境中快速开展性能验证。该工具已有718人学习/下载,适合需要模拟高负载场景的测试人员,同时提示长时间满载可能引发硬件过热,应确保散热并避开重要任务时段。总体而言,这是一套便携实用的压力测试辅助手段,能帮助快速评估系统在指定负载下的表现。
1. 刷CPU使用率不是什么玄学:先想清楚你要刷给谁看
不少从业者找 Windows 刷CPU使用率工具,以为只是把 CPU 跑满 100% 就完事。真做起来才发现,刷满容易,刷得“准”才是难点:让占用率稳稳停在 40%、60% 或 80%,并且长时间不漂移,需要同时处理线程数量、忙闲时间比、CPU 亲和性、电源策略和定时器分辨率。这个工具最常见的用途是散热测试、性能压测、远程主机保活,以及验证任务调度或监控告警阈值。适合的是两类人:一类是刚入门、想在本地写个可复现的小程序练手;另一类是熟手,需要一个不污染业务环境的负载生成器,用来验证监控曲线和告警策略。这篇就把实现思路、关键参数和容易翻车的地方一次讲透。
2. 原理先行:任务管理器怎么算出占用率,三种刷法先选型再动手
2.1 任务管理器那个百分比是怎么算出来的
Windows 内核会为每个逻辑处理器维护一个累计时间,区分“空闲时间”和“活动时间”。任务管理器每隔大约一秒去读一次这些计数,用两次读数之间的差值算出活动时间占比,显示成百分比。也就是说,它看的是“这 1 秒里 CPU 有多少时间被非空闲代码占据”,而不是某个进程的瞬时负载。
这个机制带来两个直接影响。第一,占用率是一个时间窗口内的平均值,窗口越短曲线越抖,窗口越长越平。第二,它只统计 CPU 处于“非空闲”调度状态的时间,无论你跑的是数学计算、死循环还是密集 IO,只要处理器没进空闲态,都会被算作活动时间。所以从“让任务管理器好看”这个角度看,任意忙循环都能刷出占用率,差别只在控制的精细度。
基于这个原理可以推导出刷 CPU 使用率工具的完整要求:按目标百分比分配每个时间片里的忙与闲长度,忙的长度占总长度比例越接近目标值,读数越准。而要实现“忙指定毫秒、闲指定毫秒”,绕不开 Windows 的定时器精度问题,这一步做不好,后面所有百分比都是纸上的数字。
2.2 三种刷法选型:忙循环、占空比与事件驱动
常见做法有三种,我按实际工程里见到的频率排序。
第一种是纯忙循环,一个while(true)空转。优点是代码最短,缺点是只能刷出接近 100% 的占用率,没法按目标百分比控制。适合临时看散热是否正常,不适合当通用工具。
第二种是“忙闲占空比”循环:忙跑 N 毫秒,再睡 M 毫秒,通过 N 与 M 的比值控制平均占用率。这是“刷 CPU 使用率工具”的主流实现,能用单线程或线程组模拟任意百分比,代码可读性好,也方便加反馈补偿。缺点是忙闲切换会产生周期性尖峰,任务管理器刷新间隔恰好和它错开时,会看到一条锯齿状曲线。
第三种是事件驱动型负载,不用主动空转,而是用定时器或异步回调制造密集任务。比如几十个短延迟的定时器排队触发,触发时做一点计算再重新排队。优点是负载形状更接近真实应用,适合压测 IO 和协议栈;缺点是占用率控制精度差,受线程池调度影响大,写起来也复杂。
这三种方式的选择不取决于谁更“高级”,取决于你要刷给谁看。如果只是给任务管理器看曲线,占空比循环就是最优解;如果给 APM 或监控系统压测告警规则,事件驱动型更真实。做通用工具的话,我一般会把占空比作为默认实现,再把事件型留作扩展开关。
| 刷法 | 控制精度 | 曲线平滑度 | 代码复杂度 | 适用场景 |
|---|---|---|---|---|
| 纯忙循环 | 只能 100% | 平稳 | 极低 | 散热测试、临时占满 |
| 忙闲占空比 | 高 | 有锯齿 | 低 | 按百分比刷任务管理器 |
| 事件驱动 | 低 | 平滑 | 中高 | 业务负载模拟、IO 压测 |
3. 最小可用实现:用 C# 写一个多核 CPU 负载工具并跑起来
3.1 代码骨架:起 N 个线程按占空比空转
我习惯用 C# 写这类工具,原因很实际:控制台程序几步就能跑,跨 Windows 版本兼容性好,P/Invoke 调用 Windows API 也比 C++ 省事。下面这个骨架是按“每逻辑核一个线程,每个线程各自跑忙闲循环”设计的。
using System.Diagnostics; int percent = 60; // 目标占用率 int cores = Environment.ProcessorCount; // 默认用满所有逻辑核 int tickMs = 100; // 忙闲周期的基准长度 using var cts = new CancellationTokenSource(); double intervalMs = Environment.TickCount; // 仅用于演示 for (int i = 0; i < cores; i++) { int idx = i; var t = new Thread(() => BurnLoop(percent, tickMs, cts.Token, idx)) { IsBackground = true, Name = $"BurnCore{idx}" }; t.Start(); } Console.WriteLine("按任意键退出..."); Console.ReadKey(); cts.Cancel(); static void BurnLoop(int targetPercent, int tickMs, CancellationToken ct, int idx) { long freq = Stopwatch.Frequency; // 每秒计时精度 long tickTicks = tickMs * freq / 1000L; // 一个周期对应多少计时单位 long busyTicks = tickTicks * targetPercent / 100L; while (!ct.IsCancellationRequested) { long start = Stopwatch.GetTimestamp(); // 忙段:空转消耗 CPU,持续 busyTicks while (Stopwatch.GetTimestamp() - start < busyTicks) { // 这里不需要做任何事,空循环本身就是负载 } // 闲段:让出 CPU,持续 (tickTicks - busyTicks) long elapsed = Stopwatch.GetTimestamp() - start; long remain = tickTicks - elapsed; if (remain > 0) { Thread.Sleep((int)(remain * 1000L / freq)); } } }这段代码的思路是给每个逻辑核分配一个后台线程,每个线程在固定周期里先忙转一段时间,再睡眠一段时间,忙段时间占总周期比例就是目标占用率。忙段用Stopwatch.GetTimestamp()而非DateTime.Now,因为前者直接读高精度性能计数器,不受系统时间调整影响;忙闲计算全部换算成计时单位,避免毫秒换算带来的累积误差。
参数上最需要理解的是tickMs。它越小,曲线越细腻,但 Sleep 的误差占比越大;它越大,百分比越容易稳定,但任务管理器里看到的曲线颗粒感越重,出现周期锯齿。代码里的Thread.Sleep用整数毫秒,实际睡眠时间可能被系统定时器精度拖长,这一点后面避坑章会细说。先跑通这个版本,再考虑精度优化。
3.2 编译、运行与任务管理器验证
在安装了 .NET SDK 的机器上,把上面的代码存成cpuload.cs,在同目录执行编译命令。
dotnet build -c Release -o out生成的可执行文件在out目录下,直接运行:
./out/cpuload.exe运行后打开任务管理器,切到“性能”页,观察 CPU 总占用率。目标设为 60% 时,理论上总占用率会停在 55%~65% 之间浮动,不会精确停在 60。这个偏差主要来自 Sleep 的时间误差和线程切换开销,正常现象。
我建议此时顺手做一个验证:切到“详细信息”页,找到进程对应的条目,确认上下文里能看到多个线程同时处于“运行”状态。若只有一个核在跑,说明线程没有铺满所有逻辑核,需要确认Environment.ProcessorCount是否被虚拟机或组策略限制。跑完一轮确认基本功能后,再往下调百分比控制,不要一上来就追求精确。
4. 参数调教:目标百分比、亲和性与优先级的控制细节
4.1 百分比不稳的根源与忙闲比补偿
初版跑起来后最常见的反馈是:目标 60%,实际在 40%~75% 之间乱跳。这不是代码写错了,而是 CPU 频率不恒定。现代处理器会根据负载自动调整频率,忙段一开始频率冲高、功耗上升,闲段频率回落,频率波动直接影响忙段时间的实际计算量,进而改变占用率。再加上电源计划里的节能策略和温度墙,占用率长时间跑一定往下漂。
解决办法是加一个简单的反馈补偿,把上一轮实际占用率和目标值的误差折算进下一轮忙段时间。下面这段是补偿核心。
static void BurnLoopWithCompensation(int targetPercent, int tickMs, CancellationToken ct) { long freq = Stopwatch.Frequency; double busyRatio = targetPercent / 100.0; long tickTicks = tickMs * freq / 1000L; long lastTicks = tickTicks; while (!ct.IsCancellationRequested) { long cycleStart = Stopwatch.GetTimestamp(); long busyTicks = (long)(tickTicks * busyRatio); while (Stopwatch.GetTimestamp() - cycleStart < busyTicks) { } long spent = Stopwatch.GetTimestamp() - cycleStart; long sleepTicks = tickTicks - spent; if (sleepTicks > 0) { Thread.Sleep((int)(sleepTicks * 1000L / freq)); } long total = Stopwatch.GetTimestamp() - cycleStart; double actualPercent = (double)(total - Math.Max(0, sleepTicks)) / total * 100.0; double error = targetPercent - actualPercent; busyRatio = Math.Clamp(busyRatio + error * 0.01, 0.05, 0.95); lastTicks = total; } }补偿逻辑的关键在actualPercent的估算:用忙段实际花费的时间去除以整个周期实际时间。误差为正说明实际占比偏低,下一轮就把忙段比例调大一点点。0.01是补偿步长,取值太大会在两个方向来回振荡,取值太小则要很多个周期才能修正回来。调到 0.005~0.02 之间比较稳妥,具体值取决于 tickMs 和机器频率波动程度。
注意这里刻意把busyRatio限制在 5%~95%,防止补偿在极端情况下把线程变成纯忙循环或纯睡眠。若你确实需要模拟 100% 负载,应该单独做一个死循环模式,而不是走补偿分支。
4.2 用亲和性和优先级锁定行为
多核机器上另一个隐蔽问题是线程迁移。Windows 调度器会为了均衡负载把线程从一个核挪到另一个核,结果就是任务管理器里每个核的占用率都只有一部分,看起来像是“所有核都在半忙”,不符合“指定某几个核打满”的预期。解决办法是设置线程亲和性,把每个负载线程钉在指定逻辑核上。
using System.Runtime.InteropServices; [DllImport("kernel32.dll")] static extern IntPtr GetCurrentThread(); [DllImport("kernel32.dll")] static extern IntPtr SetThreadAffinityMask(IntPtr hThread, IntPtr mask); // 绑定线程到第 idx 个逻辑核 static void BindThreadToCore(int idx) { IntPtr mask = (IntPtr)(1L << idx); SetThreadAffinityMask(GetCurrentThread(), mask); }调用时机是在线程回调的第一行,绑定之后这个线程就不会再被调度到其他核。要注意的是1L << idx在逻辑核超过 64 的机器上会溢出,遇到这类机器改用处理器组 API,普通消费级机器不用管。还有一个常见误用:直接调Process.GetCurrentProcess().ProcessorAffinity,这会把整个进程的线程都限制在固定核集合里,副作用是 .NET 后台 GC 线程也被绑定,可能造成 GC 停顿变长,所以尽量别用进程级亲和性。
优先级方面,我的习惯是把负载线程设为低于 Normal。原因很直白:刷 CPU 占用率时机器往往会跑满所有核,如果负载线程优先级高于系统关键线程,鼠标、任务管理器、远程桌面全会卡到无法操作。设置优先级的常见做法是:
Thread.BeginThreadAffinity(); try { BindThreadToCore(idx); // 设置优先级 ProcessThread? pThread = Process.GetCurrentProcess() .Threads.Cast<ProcessThread>() .FirstOrDefault(pt => pt.Id == AppDomain.GetCurrentThreadId()); if (pThread != null) { pThread.PriorityLevel = ThreadPriorityLevel.BelowNormal; } BurnLoop(targetPercent, tickMs, ct, idx); } finally { Thread.EndThreadAffinity(); }BelowNormal优先级既能保证负载线程被调度,又不会饿死交互线程。优先级设为Idle会导致占用率普遍偏低,因为忙段经常被其他线程抢先;设为AboveNormal则可能在 100% 负载时把系统搞到半死。极少情况下需要High,比如要验证系统在极端负载下的告警,平时别碰。
4.3 用配置文件驱动:无窗口版怎么跑
命令行参数适合快速验证,做成长期运行的工具后建议改用配置文件。我一般用一个极简的 JSON 文件驱动,避免每次改参数都重新编译。结构大致是这样:
{ "percent": 60, "cores": [0, 1, 2, 3], "tickMs": 100, "durationSeconds": 0, "priority": "BelowNormal" }程序启动时读文件,按cores数组里的核号逐个起线程,percent作为目标值传给补偿逻辑,durationSeconds为 0 表示一直运行,大于 0 则到时间自动退出。配置文件比参数列表好维护的地方是核号和优先级可读性好,不会出现--cores 0,1,2,3写错后进程只跑一半核的情况。
无窗口运行只需要两步:第一,把编译好的 exe 放进一个固定目录;第二,用计划任务或后台方式启动它。如果只是偶尔跑一次,直接在 PowerShell 里用后台进程是最快的:
$p = Start-Process -FilePath "D:\tools\cpuload.exe" -WindowStyle Hidden加-WindowStyle Hidden后控制台窗口不显示,但进程仍会一直运行。要注意关闭主窗口事件被隐藏后,原来的Console.ReadKey()就失效了,所以无窗口模式下必须有durationSeconds作为兜底退出条件,否则只能在任务管理器里强制结束进程,有点狼狈。
5. 避坑记录:任务管理器读数、15.6ms 定时器与温度墙
5.1 任务管理器读数的两种口径,别拿总占用率当真相
排错之前先确认一个易混淆点:任务管理器的“CPU 总占用率”是所有逻辑核的平均值。“性能”页里默认显示总占用率,只有把视图改成“逻辑处理器”才能看到单核曲线。验证刷核工具是否生效时,务必对照单核视图,否则会看到一种奇怪现象:四个核都显示 100%,总占用率却是 25% 或 50%,这取决于 Windows 在总占用率的计算里是否把多核归一化。我见过不少人在这一步误判工具失效,实际只是看错了视图。
5.2 五条高频踩坑记录:现象、原因与处理
踩坑一:Sleep(1) 实际睡了 15.6ms,低百分比完全控不住
现象:目标 10%,实际占用率在 0% 和 30% 之间大幅摆动,曲线像方波。原因是 Windows 默认定时器分辨率是 15.6ms,Thread.Sleep(1)要等一个系统 tick 才返回,忙闲比例完全失真。解决方法是把周期拉长到 100ms 以上,比如 tickMs 设为 200,这样 10% 对应忙 20ms、闲 180ms,Sleep 误差占比变小。追求更精细控制可调用timeBeginPeriod(1)把系统定时器分辨率提到 1ms,但注意这在所有程序间共享,跑完要timeEndPeriod(1)还原。
踩坑二:多个线程同时忙转导致占用率振荡
现象:8 核机器目标 50%,总占用率在 30%~70% 大幅波动,每个核自身曲线也不规则。原因是所有线程同时进入忙段、同时进入闲段,形成共振,任务管理器采样点每次落在不同相位。解决方法是给每个线程的周期起点加偏移,常见做法是启动时让每个线程先 Sleep 一个与核号成比例的随机延迟,把相位错开。相位错开后总占用率会平滑很多。
踩坑三:长时间运行后占用率缓慢下滑
现象:前 5 分钟稳定在 60%,半小时后降到 45%,风扇噪音变大。原因是温度墙和功耗限制:CPU 持续高负载后温度升高,处理器主动降频,忙段相同时间内的计算量不变但耗电变少,占用率统计反而下降。解决方法是把补偿逻辑打开,它会自动上调忙段时间对抗降频。同时确认电源计划处于“高性能”,避免节能策略在空闲段过度调低频率。这里要诚实提醒一句:任何纯用户态工具都无法完全对抗 BIOS 级别的功耗策略,只能靠补偿缓解。
踩坑四:优先级设成 High,系统卡到没法结束进程
现象:负载线程优先级过高,CPU 全被它抢走,鼠标漂移、任务管理器打不开,只能远程强制重启。原因是跑满所有核时High优先级线程几乎不让出时间片,交互线程被饿死。解决方法是把默认优先级定为BelowNormal,宁可占用率读数低几个百分点,也要保证系统可操作。若确实要模拟高优先级负载,至少保留一个逻辑核不绑负载线程。
踩坑五:任务管理器占用率显示为 0,但进程确实在空转
现象:负载线程在跑,任务管理器 CPU 总占用率却接近 0,Queries 看起来一切正常。原因一般是你在看的是“按核平均”且虚拟机只分配了 vCPU,Windows 的 CPU 占用率统计会把空闲时间也算进总核数,物理机少见但云主机常见。解决方法一样:切到逻辑处理器视图,确认每个绑定的 vCPU 是否有负载。这一点做云主机压测时尤其重要,别裸看总占用率就下结论。
6. 校准与进阶:性能计数器验证负载,再加无窗口自动退出
6.1 用性能计数器校准你的负载
任务管理器只能看个大概,要确认工具真的在按目标百分比运行,建议用系统自带的性能计数器采样。PowerShell 一条命令就能连续采 30 次:
Get-Counter -Counter "\Processor(_Total)\% Processor Time" -SampleInterval 1 -MaxSamples 30输出里能看到每秒一个采样值,把 30 个值平均一下,与目标百分比做对比。偏差在 ±5% 以内就算合格,偏差过大就回第 4 章调补偿步长和 tickMs。想看单核数据就把计数器路径换成\Processor(0)\% Processor Time这类,能顺带验证亲和性绑定是否生效。
另一种做法是用typeperf把数据导成文件,适合跑长时间校准。导出后用 Excel 画曲线,可以看出补偿是否产生了周期性过冲。我习惯在调参阶段用typeperf -si 1 -sc 300采 5 分钟,然后对比目标值和中位数,比肉眼盯任务管理器可靠得多。
6.2 无窗口自动退出的实战组合
日常使用我不建议手动跑这个工具,而是配一个计划任务,让它在指定时间启动、运行固定时长后自行退出。计划任务的创建命令如下:
schtasks /Create /TN "CPULoadProfile" /TR "D:\tools\cpuload.exe --config D:\tools\load.json" /SC ONCE /ST 22:00 /RU SYSTEM/RU SYSTEM意味着以系统账户运行,不需要登录桌面也能生效,配合配置文件里的durationSeconds实现自动退出。比如压测安排成每晚 22 点跑 30 分钟,durationSeconds设为 1800,到点进程自己结束,不影响第二天使用。
我现在的习惯是把这套流程做成三板斧:先用配置文件固定参数,再用性能计数器校准百分比,最后交给计划任务定期运行。这样做的好处是每次的结果都可对比,不会出现“上次跑得好好的,这次改了参数就翻车”的尴尬。校准工具这件事本身不难,难的是把调参、观察、验证串成一条稳定流程;把这个流程固定下来,后面无论换机器还是换用途,都能半小时内重新上手。希望帮到你。
本文还有配套的精品资源,点击获取