python并发与并行(一) ———— 用subprocess管理子进程

less than 1 minute read

Published:

并发(concurrency)指计算机似乎能在同一时刻做许多件不同的事情。例如,在只配有一个CPU核心的计算机上面,操作系统可以迅速切换这个处理器所运行的程序,因此尽管同一时刻最多只有一个程序在运行,但这些程序能够交替地使用这个核心,从而造成一种假象,让人觉得它们好像真的在同时运行。 并行(parallelism)与并发的区别在于,它强调计算机确实能够在同一时刻做许多件不同的事情。例如,若计算机配有多个CPU核心,那么它就真的可以同时执行多个程序。每个CPU核心执行的都是自己的那个程序之中的指令,这些程序能够同时向前推进。

并行与并发之间的区别,关键在于能不能提速(speedup)。如果程序把总任务量分给两条独立的执行路径去同时处理,而且这样做确实能让总时间下降到原来的一半,那么这就是并行,此时的总速度是原来的两倍。反过来说,假如无法实现加速,那即便程序里有一千条独立的执行路径,也只能叫作并发,因为这些路径虽然看起来是在同时推进,但实际上却没有产生相应的提速效果。

用subprocess管理子进程

我们可以通过subprocess来执行命令行工具,因为shell脚本越写会越复杂,所以改用python来实现,会更容易理解与维护。

import subprocess

result=subprocess.run(
    ['echo', 'hello from the child process!'],
    capture_output=True, #告诉`subprocess.run()`函数捕获命令的标准输出和标准错误

    encoding='utf-8'
)

result.check_returncode()
print(result.stdout)
import subprocess

# 这里使用 subprocess.Popen 来启动一个新的子进程。在这个例子中,子进程执行的命令是 sleep 1,该命令会使进程休眠1秒钟。Popen 是 subprocess 模块中用于启动子进程的一个类,与 subprocess.run 不同的是,Popen 提供了更多的控制和灵活性。
proc=subprocess.Popen(['sleep','1'])

# proc.poll()方法用于检查子进程的退出状态。如果子进程仍在运行,则poll()方法返回None。在这个循环中,只要子进程还在运行(即 proc.poll()返回None`),就会不断打印 'Working...'。这实际上是在模拟一种“等待”机制,虽然在实际应用中,更推荐使用事件或其他同步机制来等待进程完成。
while proc.poll() is None:
    print('Working...')

print('Exit status',proc.poll())

Output:

...
working...
Working...
Working...
Working...
Working...
Working...
Working...
Working...
Exit status 0

subprocess.Popen相比subprocess.run的优点是,使用Popen启动的子进程,可以在子进程启动之后,python的主进程可以去做其他的事情,每隔一段时间来查询一次子进程的状态即可。

subprocess.run 和 subprocess.Popen 是 Python 的 subprocess 模块中用于执行外部命令的两个主要函数/类,但它们在使用和功能上有一些显著的区别。

subprocess.run 便捷性:subprocess.run 是一个高级函数,为运行命令并等待其完成提供了一个简单的接口。它非常适合用于“一次性”命令执行,其中你不需要与长时间运行的进程进行交互。 返回值:subprocess.run 返回一个 CompletedProcess 对象,其中包含有关运行进程的信息,如返回码、标准输出和标准错误输出。 等待进程:subprocess.run 会等待进程完成,然后返回。你不需要手动管理子进程的生命周期。 用法示例:

result = subprocess.run(['ls', '-l'], capture_output=True, text=True)  
print(result.stdout)

subprocess.Popen 灵活性:subprocess.Popen 是一个类,它提供了更多的控制和灵活性。它允许你启动一个子进程并与其输入/输出/错误管道、返回码等进行交互。 异步执行:与 subprocess.run 不同,subprocess.Popen 不会等待子进程完成。你可以启动一个进程并继续在 Python 脚本中执行其他操作,同时子进程在后台运行。 手动管理:你需要手动管理 Popen 实例的生命周期,包括等待其完成和检查返回码等。 用法示例:

proc = subprocess.Popen(['ls', '-l'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)  
stdout, stderr = proc.communicate()  
print(stdout.decode())

总结 如果你只需要简单地运行一个命令并等待其完成,同时获取其输出,那么 subprocess.run 是更好的选择。 如果你需要更复杂的进程控制(例如,与子进程进行交互、在子进程运行时执行其他操作、或处理多个并行的子进程),那么 subprocess.Popen 将是更合适的选择。

把子进程从父进程中剥离,可以让程序平行地运行多条子进程。例如,我们可以像下面这样,先把需要运行的这些子进程用Popen启动起来。

import subprocess
import time
start=time.time()
sleep_procs=[]
for _ in range(10):
    proc=subprocess.Popen(['sleep','1'])
    sleep_procs.append(proc)

for proc in sleep_procs:
    # communicate() 方法会等待进程完成,并处理其标准输出和标准错误(如果有的话)。在这个例子中,sleep 命令没有输出,所以 communicate() 主要用于等待每个子进程完成。
    proc.communicate()

end=time.time()
delta=end-start
print(f'Finished in {delta:.3} seconds')

Output:

Finished in 1.01 seconds

从统计结果可以看出,这10条子进程确实表现出了平行的效果。假如它们是按顺序执行的,那么程序耗费的总时长至少应该是10秒,而不是现在看到的1秒左右.

另外,值得一提的是,在这个特定的例子中,使用 proc.communicate() 可能不是等待进程完成的最高效方法,因为它主要是用于处理进程的输出。由于 sleep 命令没有输出,所以这里使用 proc.wait() 可能更为合适,它仅仅等待进程完成而不涉及任何输出处理。不过,在这个例子中,两者之间的性能差异应该是微不足道的。