psutil函数均为同步阻塞调用,无原生async支持,直接await会阻塞事件循环;必须通过loop.run_in_executor提交至线程池执行,且cpu_percent需两次调用并注意线程安全与预热间隔。

为什么不能直接 await psutil 的函数
psutil 所有获取系统信息的函数(如 psutil.cpu_percent()、psutil.virtual_memory())都是同步阻塞调用,底层通过读取 /proc 或调用系统 API 实现,没有原生 async 版本。强行在 async 函数里直接调用它们,会阻塞整个事件循环——哪怕只采集一次,也可能拖慢其他协程。
常见错误现象:asyncio.sleep(0) 无法让出控制权,监控任务卡住,其他异步任务响应延迟升高。
- 必须用
loop.run_in_executor()将 psutil 调用提交到线程池执行 - 不要用
concurrent.futures.ProcessPoolExecutor:psutil 多数函数在子进程里行为异常(例如psutil.process_iter()可能漏进程或报PermissionError) - 线程池大小建议设为
1或2:系统监控本身是 I/O 密集型伪计算,不是 CPU 密集型,开太多线程反而增加上下文切换开销
如何安全地在 asyncio 中采集 CPU 和内存数据
关键在于分离「采样触发」和「数据获取」:事件循环负责定时调度,线程池负责执行 psutil 调用。注意 psutil.cpu_percent() 需要两次调用才能返回有效值(首次返回 0.0,第二次才计算差值),所以必须在同一线程/进程中连续调用。
实操建议:
- 用
asyncio.Lock()保护共享的 cpu_percent 上下文(避免多个协程并发调用导致内部状态错乱) - 首次调用
psutil.cpu_percent(percpu=False)后,至少等待 0.1 秒再调第二次,否则可能返回 0.0;但不要用await asyncio.sleep(0.1)—— 应该在 executor 里用time.sleep(0.1) - 内存数据(
psutil.virtual_memory())可直接调用,无需预热
示例片段(核心逻辑):
import asyncio
import psutil
from concurrent.futures import ThreadPoolExecutor
<p>executor = ThreadPoolExecutor(max_workers=1)</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill7351" title="testing-python"><img
src="https://img.php.cn/upload/skill/000/000/081/179143938488980.jpg" alt="testing-python" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill7351" title="testing-python" class="overflowclass">testing-python</a>
<p class="overflowclass">使用pytest编写和评估有效的Python测试。适用于编写测试、审查测试代码、调试测试失败或提高测试覆盖率。</p>
</div>
<a rel="nofollow" href="/xiazai/skill7351" title="testing-python" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div><p>async def get_cpu_and_memory():
loop = asyncio.get_running_loop()</p><h1>第一次 cpu_percent 调用(预热)</h1><pre class="brush:php;toolbar:false;">await loop.run_in_executor(executor, psutil.cpu_percent, False)
await asyncio.sleep(0.1) # 等待间隔,确保下次能算出变化
cpu = await loop.run_in_executor(executor, psutil.cpu_percent, False)
mem = await loop.run_in_executor(executor, psutil.virtual_memory)
return {"cpu": cpu, "memory_percent": mem.percent}
采集频率高时容易触发的 PermissionError 和 ProcessLookupError
高频调用 psutil.process_iter() 或遍历所有进程的内存信息(如 p.info['memory_info'])时,在 Linux 上常遇到 PermissionError: [Errno 13] Permission denied,或 ProcessLookupError: process no longer exists。这不是权限配置问题,而是竞态条件:进程在你获取句柄后瞬间退出,或被其他用户拥有且未授权读取。
- 永远用
try/except包裹单个进程的属性访问,而不是整个process_iter()循环 - 避免使用
psutil.process_iter(attrs=['...'])一次性拉取大量字段——它内部仍会逐个访问进程,失败概率更高;改用基础迭代 + 按需获取 - 如果只需统计总数(如总进程数、僵尸进程数),用
len(list(psutil.process_iter()))更稳妥,但注意这本身也会触发大量系统调用,应限制频率(如 ≥5 秒一次)
如何把异步监控集成进 FastAPI 或后台服务
别在 @app.on_event("startup") 里直接 asyncio.create_task() 启动无限采集循环——若未正确处理取消逻辑,服务重启时任务会泄漏,占用线程池资源。
- 用
asyncio.Task保存监控任务引用,并在 shutdown 时显式task.cancel()+await task - 线程池(
ThreadPoolExecutor)也应在 shutdown 时调用shutdown(wait=True),否则 Python 解释器退出时可能引发RuntimeError: cannot schedule new futures after shutdown - 如果用 uvicorn 运行 FastAPI,确保
workers > 1时每个 worker 进程独立初始化自己的 executor 和监控任务(psutil 不跨进程共享状态)
真正容易被忽略的一点:psutil 的某些方法(如 psutil.net_io_counters())在容器环境(Docker/K8s)中默认只能看到当前网络命名空间的数据,若需宿主机全局网络指标,需以 hostNetwork: true 或挂载 /proc 并设置 privileged: true——但这带来安全风险,多数场景应改用 cgroup v2 接口或专门的 exporter。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










