在 asyncio 子进程上调用 .terminate() 引发 ProcessLookupError
Calling .terminate() on asyncio subprocess raises ProcessLookupError
我有一些使用子进程的非异步代码...
import subprocess
import signal
p = subprocess.Popen(['/bin/true'], stdout=subprocess.PIPE)
# ... do something else here ...
# The process may or may not have finished yet.
# For the sake of this test, let us ensure a finish here
# by waiting for EOF on a pipe.
p.stdout.read()
p.terminate()
我尝试将它迁移到 asyncio。但是,.terminate()
调用会引发 ProcessLookupError
.
import asyncio
import asyncio.subprocess
import signal
async def main():
p = await asyncio.create_subprocess_exec('/bin/true',
stdout=asyncio.subprocess.PIPE)
# ... do something else here ...
# for the sake of this test, ensure a finish here
await p.stdout.read()
p.terminate()
asyncio.run(main())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib64/python3.8/asyncio/runners.py", line 43, in run
return loop.run_until_complete(main)
File "/usr/lib64/python3.8/asyncio/base_events.py", line 616, in run_until_complete
return future.result()
File "<stdin>", line 6, in main
File "/usr/lib64/python3.8/asyncio/subprocess.py", line 141, in terminate
self._transport.terminate()
File "/usr/lib64/python3.8/asyncio/base_subprocess.py", line 149, in terminate
self._check_proc()
File "/usr/lib64/python3.8/asyncio/base_subprocess.py", line 142, in _check_proc
raise ProcessLookupError()
ProcessLookupError
这段代码有什么错误?我做错了什么?
我测试了以下版本:
- python39-3.9.0-1.fc32.x86_64
- python3-3.8.5-5.fc32.x86_64
解决方法:在调用.terminate()
之前,使用p.returncode
检查进程是否已经返回。这同样适用于调用 .kill()
或 .send_signal()
.
if p.returncode is None:
p.terminate()
此代码是安全的。[*] 在检查和 .terminate()
调用之间无法“收割”进程。只能在您的异步函数等待时收割该过程(await
语句)。
[*] 我撒谎了,这不安全。查看 ThreadedChildWatcher,Unix 进程可以立即获得。这看起来是一个非常烦人的竞争条件。
讨论
在non-asyncsubprocess
模块中,调用.wait()
是收获过程,设置.returncode
。如果您没有调用 .wait()
,则不会设置 .returncode
。如果一个UNIX进程退出但还没有被收割,它就继续作为“僵尸”存在。
在asyncio
中,事件循环收获进程并设置.returncode
。这可能发生在函数中的任何 await
语句期间。目前的文档没有提到这一点。收割 Unix 进程意味着它不再存在。没有可以发送信号的对象。
理论上,可以更改asyncio
以允许问题中的代码。但是,存在 backwards-compatibility 问题。到目前为止,我怀疑某些程序依赖于 .returncode
设置 without/before .wait()
,尽管它没有被记录。为了设置.returncode
,必须回收Unix进程
最大的 backwards-compatible 变化可能是 asyncio
自己进行检查。这对使用 p.pid
调用 os.kill()
的代码没有帮助。这样的代码不太可能得到支持。 (首先,使用便携式 Unix 系统调用无法支持它,除非您删除或降级 FastChildWatcher)。
我有一些使用子进程的非异步代码...
import subprocess
import signal
p = subprocess.Popen(['/bin/true'], stdout=subprocess.PIPE)
# ... do something else here ...
# The process may or may not have finished yet.
# For the sake of this test, let us ensure a finish here
# by waiting for EOF on a pipe.
p.stdout.read()
p.terminate()
我尝试将它迁移到 asyncio。但是,.terminate()
调用会引发 ProcessLookupError
.
import asyncio
import asyncio.subprocess
import signal
async def main():
p = await asyncio.create_subprocess_exec('/bin/true',
stdout=asyncio.subprocess.PIPE)
# ... do something else here ...
# for the sake of this test, ensure a finish here
await p.stdout.read()
p.terminate()
asyncio.run(main())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib64/python3.8/asyncio/runners.py", line 43, in run
return loop.run_until_complete(main)
File "/usr/lib64/python3.8/asyncio/base_events.py", line 616, in run_until_complete
return future.result()
File "<stdin>", line 6, in main
File "/usr/lib64/python3.8/asyncio/subprocess.py", line 141, in terminate
self._transport.terminate()
File "/usr/lib64/python3.8/asyncio/base_subprocess.py", line 149, in terminate
self._check_proc()
File "/usr/lib64/python3.8/asyncio/base_subprocess.py", line 142, in _check_proc
raise ProcessLookupError()
ProcessLookupError
这段代码有什么错误?我做错了什么?
我测试了以下版本:
- python39-3.9.0-1.fc32.x86_64
- python3-3.8.5-5.fc32.x86_64
解决方法:在调用.terminate()
之前,使用p.returncode
检查进程是否已经返回。这同样适用于调用 .kill()
或 .send_signal()
.
if p.returncode is None:
p.terminate()
此代码是安全的。[*] 在检查和 .terminate()
调用之间无法“收割”进程。只能在您的异步函数等待时收割该过程(await
语句)。
[*] 我撒谎了,这不安全。查看 ThreadedChildWatcher,Unix 进程可以立即获得。这看起来是一个非常烦人的竞争条件。
讨论
在non-asyncsubprocess
模块中,调用.wait()
是收获过程,设置.returncode
。如果您没有调用 .wait()
,则不会设置 .returncode
。如果一个UNIX进程退出但还没有被收割,它就继续作为“僵尸”存在。
在asyncio
中,事件循环收获进程并设置.returncode
。这可能发生在函数中的任何 await
语句期间。目前的文档没有提到这一点。收割 Unix 进程意味着它不再存在。没有可以发送信号的对象。
理论上,可以更改asyncio
以允许问题中的代码。但是,存在 backwards-compatibility 问题。到目前为止,我怀疑某些程序依赖于 .returncode
设置 without/before .wait()
,尽管它没有被记录。为了设置.returncode
,必须回收Unix进程
最大的 backwards-compatible 变化可能是 asyncio
自己进行检查。这对使用 p.pid
调用 os.kill()
的代码没有帮助。这样的代码不太可能得到支持。 (首先,使用便携式 Unix 系统调用无法支持它,除非您删除或降级 FastChildWatcher)。