Python異步調用外部命令的踩坑實錄
最近在重構一個數據處理服務,需要并發(fā)調用十幾個外部命令行工具(ffmpeg、wkhtmltopdf之類)。本來以為把 subprocess.run() 換成 asyncio.create_subprocess_exec() 就完事了,結果踩了一串坑,分享給同樣在折騰異步子進程的同學。
坑1:在async函數里直接用 subprocess.run() 阻塞整個事件循環(huán)
這是最常犯的錯。很多人知道async函數,但習慣了同步寫法:
async def process_video(path):
# ? 這會阻塞整個事件循環(huán)!
result = subprocess.run(["ffmpeg", "-i", path, "output.mp4"], capture_output=True)
return result.stdout
subprocess.run() 是同步阻塞調用。在async函數里直接用它,整個事件循環(huán)都會卡住,其他協程全部停擺。如果你的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管道沒消費導致死鎖
這個坑極其隱蔽。當你創(chuàng)建子進程并設置了 stdout=PIPE,但忘記讀取輸出時:
async def run_tool(cmd):
proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE)
# ? 如果子進程輸出了大量數據填滿管道緩沖區(qū)(通常64KB),
# 子進程會阻塞在write()上,你的await也永遠不會返回
await proc.wait() # 死鎖!
return proc.returncode
操作系統管道緩沖區(qū)有限,子進程往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:大量并發(fā)子進程耗盡文件描述符
每個子進程至少占3個fd(stdin/stdout/stderr的管道),加上communicate的緩沖區(qū)。我一開始并發(fā)起了50個子進程,直接 OSError: [Errno 24] Too many open files。
解決方案:
# 1. 查看當前限制
import resource
print(resource.getrlimit(resource.RLIMIT_NOFILE)) # 通常1024
# 2. 用Semaphore控制并發(fā)數
sem = asyncio.Semaphore(10) # 最多10個并發(fā)子進程
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() # 發(fā)SIGKILL
await proc.wait() # 等待進程回收,避免僵尸進程
raise
注意兩點:
kill()之后一定要wait(),否則子進程變成僵尸進程占用PIDkill()發(fā)SIGKILL是強制終止,如果子進程有子子進程,它們可能變成孤兒進程。更干凈的做法是殺進程組:
import os
import signal
# 創(chuàng)建子進程時指定新的進程組
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()而不是發(fā)信號 - 路徑中的反斜杠和空格需要特殊處理
- 編碼問題: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阻塞 | 事件循環(huán)卡死 | 用create_subprocess_exec |
| 管道未消費 | 死鎖 | communicate()或DEVNULL |
| fd耗盡 | Too many open files | Semaphore控制并發(fā) |
| 子進程僵死 | 永久掛起 | wait_for超時+kill+wait |
| Windows兼容 | 各種報錯 | 條件分支+terminate |
異步子進程看起來簡單,實際上涉及操作系統管道、進程管理、信號處理等底層細節(jié)。踩完這些坑之后,我對"異步"這個概念理解深了不少——它不只是把def改成async def,而是要真正理解你的代碼在事件循環(huán)里是怎么調度的。
以上都是實際項目中遇到的問題,希望幫你少走彎路。
到此這篇關于Python異步調用外部命令的踩坑實錄的文章就介紹到這了,更多相關Python異步調用外部命令內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解決Numpy報錯:ImportError: numpy.core.multiarray faile
這篇文章主要介紹了解決Numpy報錯:ImportError: numpy.core.multiarray failed問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01
Python創(chuàng)建Getter和Setter的方法詳解
Getters?和?Setters?是幫助我們設置類變量或屬性而無需直接訪問的方法,這篇文章主要和大家介紹了如何在Python中創(chuàng)建Getter和Setter,需要的可以參考下2023-10-10
解決pycharm 工具欄Tool中找不到Run manager.py Task的問題
今天小編就為大家分享一篇解決pycharm 工具欄Tool中找不到Run manager.py Task的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-07-07
解決django中form表單設置action后無法回到原頁面的問題
這篇文章主要介紹了解決django中form表單設置action后無法回到原頁面的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
python中redis查看剩余過期時間及用正則通配符批量刪除key的方法
這篇文章主要介紹了python中redis查看剩余過期時間及用正則通配符批量刪除key的方法,需要的朋友可以參考下2018-07-07

