Python子进程返回错误的退出代码

5

我编写了一个脚本来启动多个进程(简单的单元测试)以并行方式运行。它将使用num_workers并行进程一次性完成N项工作。

我的第一个实现是将进程分为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。我检查过了,这与并发无关;由于某种原因,最近的两个作业返回了退出代码0。
为什么会发生这种情况?

3
考虑使用multiprocessing代替自己管理并行进程。 - sam
@ndmeiri 什么竞态条件?只有主线程读写 errors 变量。 - BingsF
不是竞争条件;os.wait()会覆盖poll()的返回码。 - Nathan Vērzemnieks
@NathanVērzemnieks 很敏锐! - ndmeiri
1个回答

1
问题出在os.wait()上。它不仅等待子进程退出,还返回该子进程的pid和“退出状态指示”,如文档所述。这需要等待子进程终止;但是一旦子进程终止,其返回代码对于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

Popen.poll调用的_internal_poll源代码清楚地说明了这里发生的情况:当Popen尝试在其子进程的pid上调用_waitpid时,它会得到ChildProcessError:[Errno 10] No child processes,并将自身的returncode分配为0,因为此时无法确定子进程的返回代码。

之所以仅在您的示例中的最后几个子进程中发生这种情况,是因为os.wait仅针对or assigned == N情况进行调用,并且只调用一两次,因为您的子进程非常快。如果您稍微减慢一下速度,就会得到更多的随机行为。

至于修复方法:我可能会用睡眠替换os.wait()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接