在 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

这段代码有什么错误?我做错了什么?

我测试了以下版本:

解决方法:在调用.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)。