Python异步调用外部命令的踩坑实录

 更新时间:2026年05月06日 08:29:11   作者:用户396269106003  
文章总结了在重构数据处理服务时遇到的5个异步子进程相关问题及解决方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

最近在重构一个数据处理服务,需要并发调用十几个外部命令行工具(ffmpeg、wkhtmltopdf之类)。本来以为把 subprocess.run() 换成 asyncio.create_subprocess_exec() 就完事了,结果踩了一串坑,分享给同样在折腾异步子进程的同学。

坑1:在async函数里直接用 subprocess.run() 阻塞整个事件循环

这是最常犯的错。很多人知道async函数,但习惯了同步写法:

async def process_video(path):
    # ❌ 这会阻塞整个事件循环!
    result = subprocess.run(["ffmpeg", "-i", path, "output.mp4"], capture_output=True)
    return result.stdout

subprocess.run() 是同步阻塞调用。在async函数里直接用它,整个事件循环都会卡住,其他协程全部停摆。如果你的FastAPI接口里这么写,一个请求就能把服务冻住。

正确做法是用 asyncio.create_subprocess_exec()

async def process_video(path):
    # ✅ 异步等待子进程
    proc = await asyncio.create_subprocess_exec(
        "ffmpeg", "-i", path, "output.mp4",
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    stdout, stderr = await proc.communicate()
    return stdout

坑2:stdout/stderr管道没消费导致死锁

这个坑极其隐蔽。当你创建子进程并设置了 stdout=PIPE,但忘记读取输出时:

async def run_tool(cmd):
    proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE)
    # ❌ 如果子进程输出了大量数据填满管道缓冲区(通常64KB),
    # 子进程会阻塞在write()上,你的await也永远不会返回
    await proc.wait()  # 死锁!
    return proc.returncode

操作系统管道缓冲区有限,子进程往stdout写满了就卡住,等你来读。但你只在 wait(),不去读,双方互相等——死锁。

解决方法:始终用 communicate() 同时读stdout和stderr:

async def run_tool(cmd):
    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    stdout, stderr = await proc.communicate()  # ✅ 同时消费两个管道
    return proc.returncode, stdout, stderr

如果确实不需要输出,重定向到DEVNULL:

proc = await asyncio.create_subprocess_exec(
    *cmd,
    stdout=asyncio.subprocess.DEVNULL,
    stderr=asyncio.subprocess.DEVNULL
)

坑3:大量并发子进程耗尽文件描述符

每个子进程至少占3个fd(stdin/stdout/stderr的管道),加上communicate的缓冲区。我一开始并发起了50个子进程,直接 OSError: [Errno 24] Too many open files

解决方案:

# 1. 查看当前限制
import resource
print(resource.getrlimit(resource.RLIMIT_NOFILE))  # 通常1024

# 2. 用Semaphore控制并发数
sem = asyncio.Semaphore(10)  # 最多10个并发子进程

async def run_with_limit(cmd):
    async with sem:
        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        stdout, stderr = await proc.communicate()
        return proc.returncode, stdout

# 3. 或者临时提高限制(需要权限)
# resource.setrlimit(resource.RLIMIT_NOFILE, (65536, 65536))

Semaphore是最靠谱的方式,既控制fd消耗,也避免把CPU打满。

坑4:子进程超时与僵死处理

有些命令行工具偶尔会卡死(说的就是你,wkhtmltopdf)。communicate() 本身没有超时参数(Python 3.11之前),直接await可能永远等不回来:

# ❌ 可能永远卡住
stdout, stderr = await proc.communicate()

# ✅ 用wait_for加超时
try:
    stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30.0)
except asyncio.TimeoutError:
    proc.kill()  # 发SIGKILL
    await proc.wait()  # 等待进程回收,避免僵尸进程
    raise

注意两点:

  1. kill() 之后一定要 wait(),否则子进程变成僵尸进程占用PID
  2. kill() 发SIGKILL是强制终止,如果子进程有子子进程,它们可能变成孤儿进程。更干净的做法是杀进程组:
import os
import signal

# 创建子进程时指定新的进程组
proc = await asyncio.create_subprocess_exec(
    *cmd,
    stdout=asyncio.subprocess.PIPE,
    stderr=asyncio.subprocess.PIPE,
    preexec_fn=os.setsid  # 新进程组
)

# 超时后杀整个进程组
try:
    stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30.0)
except asyncio.TimeoutError:
    os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
    await proc.wait()

坑5:Windows上的兼容性地狱

如果你的服务需要跨平台,Windows是一堆坑的集合:

  • create_subprocess_exec 在Windows上不支持 preexec_fn 参数(Windows没有进程组概念)
  • 杀进程要用 proc.terminate() 而不是发信号
  • 路径中的反斜杠和空格需要特殊处理
  • 编码问题:stdout默认是系统编码(GBK),不是UTF-8
import sys

async def run_cross_platform(cmd):
    kwargs = {
        "stdout": asyncio.subprocess.PIPE,
        "stderr": asyncio.subprocess.PIPE,
    }
    
    if sys.platform != "win32":
        kwargs["preexec_fn"] = os.setsid
    
    proc = await asyncio.create_subprocess_exec(*cmd, **kwargs)
    
    try:
        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30)
    except asyncio.TimeoutError:
        if sys.platform == "win32":
            proc.terminate()
        else:
            os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
        await proc.wait()
        raise
    
    # Windows编码处理
    if sys.platform == "win32":
        stdout = stdout.decode("gbk", errors="replace")
        stderr = stderr.decode("gbk", errors="replace")
    
    return stdout, stderr

总结

现象解法
同步subprocess阻塞事件循环卡死用create_subprocess_exec
管道未消费死锁communicate()或DEVNULL
fd耗尽Too many open filesSemaphore控制并发
子进程僵死永久挂起wait_for超时+kill+wait
Windows兼容各种报错条件分支+terminate

异步子进程看起来简单,实际上涉及操作系统管道、进程管理、信号处理等底层细节。踩完这些坑之后,我对"异步"这个概念理解深了不少——它不只是把def改成async def,而是要真正理解你的代码在事件循环里是怎么调度的。

以上都是实际项目中遇到的问题,希望帮你少走弯路。

到此这篇关于Python异步调用外部命令的踩坑实录的文章就介绍到这了,更多相关Python异步调用外部命令内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • python 函数的详解与应用范例

    python 函数的详解与应用范例

    函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。函数能提高应用的模块性,和代码的重复利用率。你已经知道Python提供了许多内建函数,比如print()。但你也可以自己创建函数,这被叫做用户自定义函数
    2021-11-11
  • 使用python 爬虫抓站的一些技巧总结

    使用python 爬虫抓站的一些技巧总结

    这篇文章主要介绍了用 python 爬虫抓站的一些技巧总结,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2018-01-01
  • python bmp转换为jpg 并删除原图的方法

    python bmp转换为jpg 并删除原图的方法

    今天小编就为大家分享一篇python bmp转换为jpg 并删除原图的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-10-10
  • Python中三种时间格式转换的方法

    Python中三种时间格式转换的方法

    本文主要介绍了Python中三种时间格式转换的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • Python利用operator模块实现对象的多级排序详解

    Python利用operator模块实现对象的多级排序详解

    python中的operator模块提供了一系列的函数操作。下面这篇文章主要给大家介绍了在Python中利用operator模块实现对象的多级排序的相关资料,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-05-05
  • 详谈python http长连接客户端

    详谈python http长连接客户端

    下面小编就为大家带来一篇详谈python http长连接客户端。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • python图形界面开发之wxPython树控件使用方法详解

    python图形界面开发之wxPython树控件使用方法详解

    这篇文章主要介绍了python图形界面开发之wxPython树控件使用方法详解,需要的朋友可以参考下
    2020-02-02
  • python字符串的常用操作方法小结

    python字符串的常用操作方法小结

    这篇文章主要为大家详细介绍了python字符串的常用操作方法,如字符串的替换、删除、截取、复制、连接、比较、查找、分割等,需要的朋友可以参考下
    2016-05-05
  • Java爬虫技术框架之Heritrix框架详解

    Java爬虫技术框架之Heritrix框架详解

    这篇文章主要介绍了爬虫技术框架之Heritrix框架详解,文中通过示例介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • python自带tkinter库实现棋盘覆盖图形界面

    python自带tkinter库实现棋盘覆盖图形界面

    这篇文章主要为大家详细介绍了python自带tkinter库实现棋盘覆盖图形界面,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-07-07

最新评论