time.time()和time.perf_counter()不准是因为它们测量墙钟时间,包含协程挂起、I/O等待等非CPU耗时;应使用time.process_time()或cProfile定位同步计算部分的CPU时间。

asyncio代码里time.time()和time.perf_counter()为什么不准?
因为它们测的是“墙钟时间”,包含协程挂起、I/O等待、事件循环调度等所有耗时,根本不能反映CPU实际执行时间。比如一个await asyncio.sleep(1)会把这1秒全算进“耗时”,但CPU其实空闲着——这会导致你误判瓶颈在CPU,实际可能只是网络延迟或锁竞争。
真正要分析CPU耗时,得用能穿透异步调度层的工具,且必须在协程真正运行的线程上下文中采样。
-
time.process_time()是正确起点:它只统计当前线程的CPU时间,忽略sleep、I/O阻塞、被抢占等非CPU活动 - 但要注意:asyncio默认在主线程跑事件循环,所以
process_time()对整个asyncio.run()调用有效;若用了loop.run_in_executor(),就得在executor线程里单独测 - 别用
psutil.Process().cpu_times().user——它返回进程级累计值,无法对齐单个协程生命周期
如何用cProfile配合asyncio做函数级CPU耗时定位?
cProfile本身不感知async/await,但它能准确记录每个同步函数调用的CPU时间(tottime列),只要你在协程内部触发的计算逻辑是同步的——这恰恰覆盖了绝大多数CPU密集场景(如JSON解析、正则匹配、数值计算)。
关键在于启动时机和作用域控制:
立即学习“Python免费学习笔记(深入)”;
- 在目标协程入口处调用
cProfile.Profile().enable(),出口处.disable(),避免污染其他协程或事件循环开销 - 不要在
asyncio.run()外层启用,否则会把事件循环初始化、回调注册等杂项全包进去 - 示例:
import cProfile import asyncio <p>async def cpu_heavy_task(): pr = cProfile.Profile() pr.enable()</p><h1>这里放你想分析的同步计算代码</h1><pre class="brush:php;toolbar:false;">result = sum(i * i for i in range(10**6)) pr.disable() pr.dump_stats("cpu_heavy_task.prof") return result
macOS UI 自动化技能包 —— 用 Python 控制你的 Mac下载让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
为什么line_profiler对async函数支持有限?
line_profiler依赖Python的sys.settrace(),而asyncio协程切换时trace hook会被重置或跳过,导致大量行号丢失、耗时归零或错位到await语句上——这不是bug,是CPython trace机制与协程调度器的固有冲突。
可行方案只有两个:
- 把CPU密集逻辑抽成独立同步函数,在里面加
@profile装饰器(需确保该函数不包含await) - 改用
py-spy record -o profile.svg --pid $PID:它通过外部进程采样,绕过trace hook,能准确捕获async协程中正在执行的Python帧及其CPU时间 - 注意:
py-spy需要目标进程未被冻结(如没加sys.settrace),且Linux/macOS支持更好;Windows下采样精度略低
多协程并发时,如何区分每个任务的CPU耗时?
直接对每个async def函数做process_time()差值是最轻量、最可靠的方式,前提是你要手动管理时间戳点。
核心技巧是利用asyncio.current_task()绑定上下文:
- 在协程开头记下
start = time.process_time(),结尾算差值,并把结果存入asyncio.current_task().get_coro().__name__或自定义task属性 - 避免用
asyncio.create_task()后立刻await——这样多个task的CPU时间会串在一起;改用asyncio.gather()并逐个await,或用asyncio.wait()配合done集合遍历 - 如果任务数量大,建议用
contextvars.ContextVar存时间戳,比全局dict更安全,防止协程间污染
真正难的不是测单个协程,而是当CPU耗时分散在多个run_in_executor子线程、或嵌套了asyncio.to_thread()时——这时必须在线程入口处重新开始process_time()计时,且结果要打上明确来源标签,否则你会完全搞不清哪段CPU时间属于哪个逻辑单元。


















