Python 子进程 returns 错误的退出代码
Python subprocess returns wrong exit code
我编写了一个脚本来并行启动多个进程(简单的单元测试)运行。它将一次执行 N
个具有 num_workers
个并行进程的作业。
我的第一个实现 运行 分批处理 num_workers
并且似乎工作正常(我在这里使用 false
命令来测试行为)
import subprocess
errors = 0
num_workers = 10
N = 100
i = 0
while i < N:
processes = []
for j in range(i, min(i+num_workers, N)):
p = subprocess.Popen(['false'])
processes.append(p)
[p.wait() for p in processes]
exit_codes = [p.returncode for p in processes]
errors += sum(int(e != 0) for e in exit_codes)
i += num_workers
print(f"There were {errors}/{N} errors")
但是,测试不会花费相同的时间,所以我有时会等待一个缓慢的测试完成。因此我重写了它以在任务完成时继续分配任务
import subprocess
import os
errors = 0
num_workers = 40
N = 100
assigned = 0
completed = 0
processes = set()
while completed < N:
if assigned < N:
p = subprocess.Popen(['false'])
processes.add((assigned, p))
assigned += 1
if len(processes) >= num_workers or assigned == N:
os.wait()
for i, p in frozenset(processes):
if p.poll() is not None:
completed += 1
processes.remove((i, p))
err = p.returncode
print(i, err)
if err != 0:
errors += 1
print(f"There were {errors}/{N} errors")
然而,这对最后几个进程产生了错误的结果。例如,在上面的示例中,它产生了 98/100 个错误而不是 100 个。我检查了一下,这与并发性无关;出于某种原因,最近的 2 个作业以退出代码 0 返回。
为什么会这样?
问题出在 os.wait()
。它不仅等待 child 进程退出:它还 return 那个 child 的 pid 和 "exit status indication",正如 the documentation 所说。这需要等到 child 进程终止;但是一旦 child 终止,它的 return 代码就不再可用于 poll
。这是重现问题的简单测试:
false_runner.py
import os
import subprocess
p = subprocess.Popen(['false'], stderr=subprocess.DEVNULL)
pid, retcode = os.wait()
print("From os.wait: {}".format(retcode))
print("From popen object before poll: {}".format(p.returncode))
p.poll()
print("From popen object after poll: {}".format(p.returncode))
输出
njv@organon:~/tmp$ python false_runner.py
From os.wait: 256
From Popen object before poll: None
From Popen object after poll: 0
The source code for _internal_poll
, called by Popen.poll
,清楚说明这里发生了什么:当 Popen
试图在其 child 进程的 pid 上调用 _waitpid
时,它得到 ChildProcessError: [Errno 10] No child processes
,并为自己分配 returncode
0,因为此时无法确定 child 进程的 return 代码。
在您的示例中仅在最后几个子流程中发生这种情况的原因是因为 os.wait
仅在 or assigned == N
情况下被调用,而且只有一两次,因为您的子流程是如此快速地。如果你放慢一点,你会得到更多的随机行为。
至于修复:我可能只是用睡眠代替 os.wait()
。
我编写了一个脚本来并行启动多个进程(简单的单元测试)运行。它将一次执行 N
个具有 num_workers
个并行进程的作业。
我的第一个实现 运行 分批处理 num_workers
并且似乎工作正常(我在这里使用 false
命令来测试行为)
import subprocess
errors = 0
num_workers = 10
N = 100
i = 0
while i < N:
processes = []
for j in range(i, min(i+num_workers, N)):
p = subprocess.Popen(['false'])
processes.append(p)
[p.wait() for p in processes]
exit_codes = [p.returncode for p in processes]
errors += sum(int(e != 0) for e in exit_codes)
i += num_workers
print(f"There were {errors}/{N} errors")
但是,测试不会花费相同的时间,所以我有时会等待一个缓慢的测试完成。因此我重写了它以在任务完成时继续分配任务
import subprocess
import os
errors = 0
num_workers = 40
N = 100
assigned = 0
completed = 0
processes = set()
while completed < N:
if assigned < N:
p = subprocess.Popen(['false'])
processes.add((assigned, p))
assigned += 1
if len(processes) >= num_workers or assigned == N:
os.wait()
for i, p in frozenset(processes):
if p.poll() is not None:
completed += 1
processes.remove((i, p))
err = p.returncode
print(i, err)
if err != 0:
errors += 1
print(f"There were {errors}/{N} errors")
然而,这对最后几个进程产生了错误的结果。例如,在上面的示例中,它产生了 98/100 个错误而不是 100 个。我检查了一下,这与并发性无关;出于某种原因,最近的 2 个作业以退出代码 0 返回。
为什么会这样?
问题出在 os.wait()
。它不仅等待 child 进程退出:它还 return 那个 child 的 pid 和 "exit status indication",正如 the documentation 所说。这需要等到 child 进程终止;但是一旦 child 终止,它的 return 代码就不再可用于 poll
。这是重现问题的简单测试:
false_runner.py
import os
import subprocess
p = subprocess.Popen(['false'], stderr=subprocess.DEVNULL)
pid, retcode = os.wait()
print("From os.wait: {}".format(retcode))
print("From popen object before poll: {}".format(p.returncode))
p.poll()
print("From popen object after poll: {}".format(p.returncode))
输出
njv@organon:~/tmp$ python false_runner.py
From os.wait: 256
From Popen object before poll: None
From Popen object after poll: 0
The source code for _internal_poll
, called by Popen.poll
,清楚说明这里发生了什么:当 Popen
试图在其 child 进程的 pid 上调用 _waitpid
时,它得到 ChildProcessError: [Errno 10] No child processes
,并为自己分配 returncode
0,因为此时无法确定 child 进程的 return 代码。
在您的示例中仅在最后几个子流程中发生这种情况的原因是因为 os.wait
仅在 or assigned == N
情况下被调用,而且只有一两次,因为您的子流程是如此快速地。如果你放慢一点,你会得到更多的随机行为。
至于修复:我可能只是用睡眠代替 os.wait()
。