用Python圆周率计算进行CPU压力测试:多进程实现与监控
发布时间:2026/10/13 1:16:51 锦皓数字建站

1. 为什么用圆周率计算来做CPU压力测试1.1 一个看似“无用”却极其精准的测试思路很多人第一次听到“用圆周率计算来压CPU”的时候反应都是这玩意儿能测出什么跑个分不就行了但真正在运维和硬件测试一线待过的人都知道跑分软件测的是“峰值性能”而压力测试要的是“持续满载下的稳定性”。这两件事完全不是一回事。圆周率计算恰好具备几个非常适合做压力测试的特质。第一它是纯计算密集型任务几乎不涉及磁盘IO和网络IO能把压力精准地压在CPU的算术逻辑单元上。第二它的计算过程可以无限迭代你想压多久就压多久不存在“跑完了没压满”的尴尬。第三它的算法可以自由调节精度和迭代次数也就是说压力的“强度”是可以量化的、可调的。第四Python虽然本身执行效率不高但正因为如此它反而能制造出非常稳定的高负载——CPU几乎一直在做浮点运算和循环控制不会出现忽高忽低的波动。我最早接触这个思路是在给一批二手服务器做稳定性筛选的时候。那批机器来源杂有的跑分很好看但连续跑几个小时高负载就蓝屏或者降频。后来我用Python写了一个圆周率计算的脚本让每台机器连续跑8小时结果一下子筛出了三台有散热问题的机器。从那以后这个看起来“简陋”的方法就成了我工具箱里的常备手段。1.2 这个方案适合谁能解决什么问题这个方案适合的人群其实比想象中广。如果你是运维工程师需要批量测试新到货的服务器或者云主机如果你是硬件爱好者想验证自己超频后的机器是否稳定如果你是开发者想测试多线程程序在满载情况下的表现甚至如果你只是想知道自己笔记本的散热到底行不行这个方案都能用。它能解决的问题也很明确第一验证CPU在持续满载下是否会降频、死机或报错第二对比不同机器或不同散热条件下的性能差异第三作为多线程编程的练手项目理解GIL全局解释器锁对CPU密集型任务的影响第四在没有专业测试工具的环境下快速搭建一个可定制的压力测试环境。需要提前说明的是这个方案不会告诉你CPU的具体型号参数也不会给你一个“跑分”它给你的是一段时间内CPU的“工作状态记录”。你要关注的是计算速度是否稳定、温度是否可控、有没有出现计算错误。这些信息对于判断一台机器是否“靠谱”比一个孤零零的分数有用得多。2. 核心原理拆解圆周率计算为什么能压住CPU2.1 从数学公式到CPU负载的转化逻辑圆周率的计算方法有很多种但适合做压力测试的必须满足两个条件计算过程要足够“重”且每一步都依赖前一步的结果这样才能让CPU一直处于忙碌状态。目前常用的有三种方法我分别说一下它们的特性和适用场景。第一种是莱布尼茨级数公式是 π/4 1 - 1/3 1/5 - 1/7 ...。这个公式极其简单收敛速度慢得令人发指但正因为慢它非常适合做长时间压力测试。你只要设定一个很大的迭代次数CPU就会一直做除法、加法和减法。它的优点是代码简单、不容易出错缺点是精度提升太慢跑半天也就算出几位小数。第二种是蒙特卡洛方法在一个正方形里随机撒点统计落在内切圆里的比例来估算π。这个方法涉及大量随机数生成和浮点运算对CPU的浮点单元压力很大。但它的缺点是结果有随机性每次跑出来的精度不一样不太适合做“可复现”的测试。第三种是Chudnovsky算法或者Machin公式这些是高精度计算π的常用方法收敛速度极快但实现复杂涉及大数运算。用它们做压力测试CPU的负载模式会偏向整数运算和内存访问和纯浮点压力测试的侧重点不同。我个人的选择是用莱布尼茨级数做长时间稳定性测试用蒙特卡洛方法做短时间高强度的浮点压力测试。这两种方法配合使用基本能覆盖大部分场景。2.2 Python的GIL对压力测试的影响这里必须专门讲一下Python的GIL问题因为很多人写多线程压力测试脚本时会踩这个坑。GIL全局解释器锁的存在使得同一时刻只有一个线程能执行Python字节码。这意味着如果你用多线程跑圆周率计算CPU的多个核心并不会同时满载——实际上只有一个核心在真正干活其他核心都在等锁。我实测过在一个4核8线程的机器上用4个线程跑莱布尼茨级数CPU总占用率只有25%左右也就是只有一个核心跑满了。这显然达不到压力测试的目的。解决方案有两个一是用多进程代替多线程每个进程有独立的Python解释器和GIL可以真正并行二是用C扩展或者NumPy这类底层释放GIL的库来执行计算。对于压力测试来说多进程是最简单直接的选择因为我们的目的就是让CPU满载而不是追求计算效率。注意如果你用多进程每个进程都会占用一份内存所以进程数不要超过物理核心数太多否则会开始吃交换分区反而影响测试结果。2.3 压力强度的量化迭代次数与时间的关系做压力测试最怕的就是“凭感觉”。我们需要一个可量化的指标来知道当前的负载到底有多重。对于莱布尼茨级数来说迭代次数就是最直接的量化指标。我做过一组实测数据在一台搭载某型号4核CPU的机器上单进程Python跑莱布尼茨级数每秒钟大约能完成800万到1200万次迭代具体取决于CPU频率和Python版本。这个数字可以作为基准用来估算你需要设置多大的迭代次数。比如你想让单个核心满载运行10分钟那就是600秒按每秒1000万次迭代算大约需要60亿次迭代。这个数字看起来很大但在Python里就是一个整数参数而已。你可以把这个参数做成命令行输入方便调整测试时长。更精确的做法是先跑一个短时间的基准测试比如10秒记录实际完成的迭代次数然后根据目标时长按比例放大。这样即使在不同机器上也能保证测试时长的一致性。3. 动手实现从单进程到多进程的完整脚本3.1 单进程版本先跑通最基本的逻辑我们先从最简单的单进程版本开始把核心逻辑跑通再逐步加功能。这个版本的目标是给定一个迭代次数计算π的近似值并输出耗时和计算结果。import time import sys def calculate_pi_leibniz(iterations): 使用莱布尼茨级数计算圆周率 iterations: 迭代次数越大越精确CPU负载越重 pi_approx 0.0 sign 1.0 denominator 1.0 for i in range(iterations): pi_approx sign / denominator sign -sign denominator 2.0 return pi_approx * 4.0 def main(): # 默认迭代1亿次大约需要10-20秒 iterations int(sys.argv[1]) if len(sys.argv) 1 else 100_000_000 print(f开始计算迭代次数: {iterations:,}) start_time time.time() pi_value calculate_pi_leibniz(iterations) elapsed time.time() - start_time print(f计算完成耗时: {elapsed:.2f} 秒) print(fπ的近似值: {pi_value:.15f}) print(f每秒迭代次数: {iterations / elapsed:,.0f}) if __name__ __main__: main()这段代码有几个细节值得说明。第一我用sign和denominator两个变量来交替加减而不是用(-1)**i因为幂运算在Python里比简单的符号翻转慢很多。第二我用了100_000_000这种下划线分隔的写法纯粹是为了可读性Python 3.6以上都支持。第三输出里包含了“每秒迭代次数”这个指标比单纯的耗时更有参考价值因为它排除了迭代次数不同的干扰。跑一下这个脚本你会看到CPU占用率直接飙到25%左右在4核机器上因为只有一个核心在干活。这就是单进程的局限但也是我们理解多进程的基础。3.2 多进程版本让所有核心都跑起来单进程版本跑通之后多进程版本就水到渠成了。核心思路是用multiprocessing模块创建多个进程每个进程跑相同或不同的迭代次数最后汇总结果。import time import sys import multiprocessing import os def calculate_pi_leibniz(iterations): 莱布尼茨级数计算圆周率返回(π值, 耗时) pi_approx 0.0 sign 1.0 denominator 1.0 start time.time() for i in range(iterations): pi_approx sign / denominator sign -sign denominator 2.0 elapsed time.time() - start return pi_approx * 4.0, elapsed def worker(args): 每个进程执行的函数 process_id, iterations args pid os.getpid() print(f[进程 {process_id}] PID{pid} 启动迭代次数: {iterations:,}) pi_value, elapsed calculate_pi_leibniz(iterations) print(f[进程 {process_id}] PID{pid} 完成耗时: {elapsed:.2f}秒π≈{pi_value:.10f}) return process_id, elapsed, pi_value def main(): # 默认使用所有CPU核心 cpu_count multiprocessing.cpu_count() process_count int(sys.argv[1]) if len(sys.argv) 1 else cpu_count iterations_per_process int(sys.argv[2]) if len(sys.argv) 2 else 50_000_000 print(fCPU核心数: {cpu_count}) print(f启动进程数: {process_count}) print(f每进程迭代次数: {iterations_per_process:,}) print(- * 50) # 准备参数 tasks [(i, iterations_per_process) for i in range(process_count)] start_time time.time() # 创建进程池并执行 with multiprocessing.Pool(processesprocess_count) as pool: results pool.map(worker, tasks) total_elapsed time.time() - start_time print(- * 50) print(f全部完成总耗时: {total_elapsed:.2f}秒) # 统计信息 avg_elapsed sum(r[1] for r in results) / len(results) print(f平均单进程耗时: {avg_elapsed:.2f}秒) print(f总迭代次数: {process_count * iterations_per_process:,}) print(f整体每秒迭代次数: {process_count * iterations_per_process / total_elapsed:,.0f}) if __name__ __main__: main()这个版本有几个关键点需要解释。第一multiprocessing.Pool会自动管理进程的创建和回收比手动Process更省心。第二pool.map会把任务列表分发给各个进程每个进程独立执行worker函数。第三每个进程的elapsed是独立计算的而total_elapsed是墙钟时间两者的差异能反映出进程间的资源竞争情况。实测下来在4核机器上跑4个进程CPU占用率能稳定在95%以上所有核心都跑满了。这时候你摸一下机器出风口能明显感觉到热浪——这才是真正的压力测试。3.3 参数调优迭代次数、进程数与测试时长的关系多进程版本跑起来之后下一个问题就是参数怎么设这里我给出一个实用的调参流程。第一步先确定测试时长。你是想跑5分钟快速验证还是跑8小时做稳定性筛选这个决定了总迭代次数。第二步跑一个基准测试。用单进程跑1000万次迭代记录耗时。假设耗时是1.2秒那么每秒迭代次数就是约833万次。第三步计算每进程迭代次数。如果你要跑10分钟600秒4个进程那么每进程需要跑600秒即600 × 833万 ≈ 50亿次迭代。这个数字就是你要设置的iterations_per_process。第四步留出余量。实际跑的时候由于多进程竞争内存带宽和缓存单进程速度可能会下降10%到20%。所以你可以把迭代次数再乘以1.2确保测试时长足够。下面这个表格是我在一台4核机器上实测的数据供你参考进程数每进程迭代次数总迭代次数实测总耗时CPU平均占用11亿1亿12.3秒25%21亿2亿13.1秒50%41亿4亿14.8秒97%81亿8亿28.5秒98%从表里能看出两个规律第一进程数从1增加到4总耗时只增加了20%但总计算量翻了4倍说明多进程确实在并行干活。第二进程数超过物理核心数4核跑8进程之后总耗时几乎翻倍这是因为操作系统在频繁切换进程上下文反而降低了效率。所以进程数不要超过物理核心数这是铁律。4. 测试过程中的监控与数据记录4.1 怎么在测试时看CPU温度和频率光跑脚本不看监控等于白跑。压力测试的核心价值在于“观察”你得知道CPU在满载时的真实状态。不同操作系统下监控手段不一样我分别说一下。在Linux环境下最直接的工具是top和sensors。top按1可以展开每个核心的占用率按P按CPU排序。sensors需要先安装lm-sensors包然后运行sensors-detect配置之后直接敲sensors就能看到每个核心的温度和风扇转速。我通常会把sensors的输出重定向到一个日志文件每隔几秒记录一次测试结束后画成曲线。在Windows环境下可以用任务管理器的“性能”标签页看CPU占用和频率但温度需要第三方工具。我常用的是HWiNFO它能记录CPU温度、频率、功耗等几十项指标并且支持导出CSV。跑测试的时候开着它测试结束后把CSV拖进Excel一目了然。在macOS环境下sudo powermetrics能看到CPU频率和功耗但温度需要istats或者TG Pro这类工具。不过Mac的散热设计通常比较保守压力测试时降频会比较明显这一点要有心理预期。提示不管用什么工具建议在测试开始前先记录5分钟的“空闲状态”数据作为基线这样对比起来才有意义。否则你看到CPU温度80度也不知道是本来就高还是压力测试导致的。4.2 记录哪些数据才有分析价值跑一次压力测试至少要记录以下几类数据否则测试结果没有参考价值。第一类是时间戳精确到秒就行。这是所有数据的基础用来对齐不同来源的记录。第二类是CPU占用率包括总体占用率和每个核心的占用率。总体占用率反映负载是否均匀单核心占用率能看出是否有“偷懒”的核心。第三类是CPU频率。这是判断是否降频的关键指标。如果频率从标称的3.5GHz掉到了2.0GHz说明散热跟不上机器在自我保护。第四类是CPU温度。这个不用多说温度过高会导致降频甚至关机。第五类是计算进度。也就是每秒迭代次数或者每完成多少迭代记录一次时间。这个数据能反映计算速度是否稳定如果中途突然变慢可能是遇到了内存交换或者温度墙。我通常会把这几类数据合并到一个CSV文件里格式大概是timestamp, cpu_percent, cpu_freq_mhz, cpu_temp_c, iterations_per_sec。测试结束后用Python的pandas或者直接Excel画图一眼就能看出问题。4.3 一个实用的监控脚本模板为了省事我写了一个简单的监控脚本在Linux下用psutil库采集数据每5秒记录一次。这个脚本可以独立运行也可以和压力测试脚本一起启动。import psutil import time import csv from datetime import datetime def monitor_cpu(duration_seconds, interval5, output_filecpu_monitor.csv): 监控CPU状态并记录到CSV文件 duration_seconds: 监控总时长 interval: 采样间隔秒 output_file: 输出文件名 end_time time.time() duration_seconds with open(output_file, w, newline) as f: writer csv.writer(f) writer.writerow([timestamp, cpu_percent, cpu_freq_mhz, per_cpu_percent]) while time.time() end_time: timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) cpu_percent psutil.cpu_percent(interval1) # 获取CPU频率部分系统可能不支持 try: freq psutil.cpu_freq() cpu_freq freq.current if freq else 0 except Exception: cpu_freq 0 # 每个核心的占用率 per_cpu psutil.cpu_percent(percpuTrue) writer.writerow([timestamp, cpu_percent, cpu_freq, per_cpu]) f.flush() # 等待到下一个采样点 time.sleep(max(0, interval - 1)) print(f监控数据已保存到 {output_file}) if __name__ __main__: import sys duration int(sys.argv[1]) if len(sys.argv) 1 else 300 monitor_cpu(duration)这个脚本依赖psutil库安装命令是pip install psutil。它每5秒采集一次数据包括总体占用率、频率和每个核心的占用率。跑完之后你可以用Excel打开CSV插入折线图CPU的“工作状态”就一目了然了。需要注意的是psutil.cpu_freq()在部分虚拟机和容器环境下会返回None这时候频率就记录为0。如果你是在云主机上测试频率数据可能拿不到那就重点关注占用率和计算速度的变化。5. 常见问题与排查技巧实录5.1 为什么CPU占用率上不去这是最常见的问题。你明明开了4个进程但任务管理器里CPU占用率只有30%到40%。原因通常有以下几个。第一个原因是进程数不够。如果你只开了1个进程那在4核机器上占用率就是25%左右这是正常的。解决办法是增加进程数但不要超过物理核心数。第二个原因是Python版本问题。Python 3.12之前的版本GIL的释放策略比较保守某些情况下即使多进程也会出现“一个进程干活其他进程围观”的现象。我实测过Python 3.12在多进程CPU密集型任务上的表现比3.10好不少。如果你有条件建议用3.12或更高版本。第三个原因是计算任务太轻。如果你设置的迭代次数太少进程很快就跑完了CPU还没来得及满载就结束了。解决办法是增加迭代次数让每个进程至少跑30秒以上。第四个原因是系统电源管理策略。有些笔记本在电池模式下会限制CPU性能即使你插着电电源计划也可能设置为“节能”。在Windows下把电源计划改成“高性能”在Linux下用cpupower frequency-set -g performance把调频策略设为性能模式。5.2 计算速度突然变慢是怎么回事测试过程中如果发现每秒迭代次数突然下降通常有以下几种可能。第一种可能是温度墙。CPU温度达到阈值通常是95度或100度后会自动降频来保护自己。这时候你会看到频率下降、计算速度变慢。解决办法是改善散热比如清理风扇灰尘、更换硅脂、增加机箱风扇。第二种可能是内存交换。如果你开的进程太多或者迭代次数设置得太大导致内存不够用系统会开始使用交换分区。交换分区的读写速度比内存慢几个数量级计算速度会断崖式下跌。解决办法是减少进程数或者增加物理内存。第三种可能是其他程序抢资源。测试的时候最好关掉浏览器、聊天软件、自动更新等后台程序。我踩过一次坑Windows在后台偷偷下载更新导致测试结果完全不可用。第四种可能是CPU降频策略。有些CPU在持续高负载下会主动降低频率来省电这叫“功耗墙”。这种情况在轻薄本上特别常见解决办法是接受现实或者换一台散热更好的机器。5.3 多进程脚本报错怎么排查多进程脚本比单进程脚本更容易出问题常见的报错和解决办法我整理成了下面这个表格。报错信息可能原因解决办法OSError: [Errno 12] Cannot allocate memory进程数太多内存不够减少进程数或增加内存AttributeError: Cant pickle local object在Windows下worker函数定义在main函数内部把worker函数移到模块顶层BrokenPipeError子进程异常退出父进程还在等待检查子进程是否有未捕获的异常RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase在Windows下没有用if __name__ __main__:保护把多进程代码放在main保护块内程序卡住不动进程池死锁通常是worker函数里有阻塞操作检查worker函数确保没有无限等待其中最容易踩的是Windows下的pickle问题。在Linux和macOS上多进程默认用fork方式创建子进程直接继承父进程的内存空间不需要序列化。但在Windows上只能用spawn方式子进程需要重新导入模块所以worker函数必须定义在模块顶层不能是嵌套函数或lambda。这个坑我踩过不止一次每次换到Windows环境都要重新提醒自己。5.4 测试结果怎么解读跑完测试拿到数据下一步就是解读。我通常关注三个指标稳定性、散热能力和性能一致性。稳定性看的是计算速度的波动。如果每秒迭代次数在测试过程中基本平稳波动不超过5%说明机器很稳。如果波动超过20%或者中途出现断崖式下跌说明有问题。散热能力看的是温度和频率的关系。如果温度稳定在80度以下频率保持在标称值附近说明散热合格。如果温度冲到95度以上频率掉到标称值的60%以下说明散热需要改进。性能一致性看的是多进程之间的差异。如果4个进程的耗时相差不超过10%说明核心之间的性能比较均衡。如果某个进程明显慢很多可能是那个核心有体质问题或者被其他任务干扰了。我一般会把测试结果分成三档优秀速度稳定、温度低于80度、无降频、合格速度基本稳定、温度80到90度、轻微降频、不合格速度波动大、温度超过90度、明显降频。这个标准不是绝对的但能快速给出一个判断。6. 进阶玩法让压力测试更贴近真实场景6.1 混合负载测试CPU加内存一起压纯CPU压力测试只能测出CPU的稳定性但真实场景下CPU往往和内存一起工作。如果你想做更全面的测试可以在圆周率计算的同时加入内存读写操作。一个简单的做法是在每次迭代中除了做浮点运算还往一个预分配的列表里写入数据。这样CPU既要算又要访问内存负载模式更接近真实应用。def calculate_pi_with_memory_pressure(iterations, memory_buffer_size10000): 带内存压力的圆周率计算 pi_approx 0.0 sign 1.0 denominator 1.0 # 预分配内存缓冲区 buffer [0.0] * memory_buffer_size buffer_index 0 for i in range(iterations): pi_approx sign / denominator sign -sign denominator 2.0 # 每1000次迭代写一次内存 if i % 1000 0: buffer[buffer_index] pi_approx buffer_index (buffer_index 1) % memory_buffer_size return pi_approx * 4.0这个版本的计算速度会比纯计算慢一些但CPU和内存的负载都上去了。你可以通过调整memory_buffer_size来控制内存压力的大小。缓冲区越大内存访问越分散对缓存的压力越大。6.2 定时启停模拟间歇性高负载有些场景下CPU不是一直满载而是间歇性高负载。比如Web服务器在高峰期处理请求闲时几乎空闲。要模拟这种场景可以写一个定时启停的脚本。import time import multiprocessing import random def burst_worker(burst_duration, idle_duration, total_duration): 间歇性压力测试忙一阵歇一阵 end_time time.time() total_duration while time.time() end_time: # 忙阶段 busy_end time.time() burst_duration while time.time() busy_end: # 执行一段计算 _ sum(i * i for i in range(10000)) # 歇阶段 time.sleep(idle_duration) def main(): process_count multiprocessing.cpu_count() total_duration 300 # 总共跑5分钟 processes [] for i in range(process_count): # 每个进程的忙闲节奏略有不同更接近真实场景 burst random.uniform(2, 5) idle random.uniform(1, 3) p multiprocessing.Process( targetburst_worker, args(burst, idle, total_duration) ) processes.append(p) p.start() for p in processes: p.join() print(间歇性压力测试完成) if __name__ __main__: main()这个脚本会让每个进程随机地忙一阵、歇一阵整体CPU占用率会在60%到100%之间波动。这种模式对电源管理和散热系统的考验更大因为CPU需要频繁地升降频。6.3 结果可视化用图表说话数据记录下来了但一堆CSV数字看起来太累。我通常会用matplotlib画几张图直观地展示测试结果。import pandas as pd import matplotlib.pyplot as plt def plot_cpu_monitor(csv_file): 绘制CPU监控数据图表 df pd.read_csv(csv_file) df[timestamp] pd.to_datetime(df[timestamp]) fig, axes plt.subplots(3, 1, figsize(12, 8), sharexTrue) # 图1CPU占用率 axes[0].plot(df[timestamp], df[cpu_percent], colorblue) axes[0].set_ylabel(CPU占用率 (%)) axes[0].set_title(CPU压力测试监控数据) axes[0].grid(True, alpha0.3) # 图2CPU频率 axes[1].plot(df[timestamp], df[cpu_freq_mhz], colorgreen) axes[1].set_ylabel(CPU频率 (MHz)) axes[1].grid(True, alpha0.3) # 图3每个核心的占用率取第一个核心作为示例 # 注意per_cpu字段是列表字符串需要解析 # 这里简化处理实际使用时需要根据CSV格式调整 axes[2].set_ylabel(单核心占用率 (%)) axes[2].set_xlabel(时间) axes[2].grid(True, alpha0.3) plt.tight_layout() plt.savefig(cpu_test_result.png, dpi150) plt.show() if __name__ __main__: plot_cpu_monitor(cpu_monitor.csv)这张图能让你一眼看出CPU占用率是否稳定、频率是否下降、温度是否过高。如果频率曲线在某个时间点突然下跌而温度曲线同时冲高那就是典型的温度墙。如果频率一直很低但温度也不高那可能是电源策略限制了性能。我个人在实际操作中的体会是压力测试的价值不在于“跑出一个分数”而在于“发现异常”。一台机器跑分高不高其实没那么重要重要的是它在持续高负载下会不会出问题。圆周率计算这个方案胜在简单、可控、可复现不需要安装任何专业软件一个Python脚本就能搞定。你可以在任何有Python环境的机器上跑也可以根据需要调整压力强度和测试时长。对于日常的稳定性验证来说这套方法足够用了。最后再分享一个小技巧如果你要测试多台机器可以把脚本和监控工具打包成一个压缩包写一个一键启动的批处理或shell脚本这样批量测试的时候能省不少事。测试结果统一保存到以机器编号命名的文件夹里方便后续对比分析。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。