Chapter13_管理子进程
1、什么时候用subprocess, 用subprocess有什么好处?
subprocess模块是Python标准库的一部分,它允许你启动新的进程、连接到它们的输入/输出/错误管道,并且获取它们的返回值。以下是一些使用subprocess模块的情况和它的好处:
使用subprocess的情况:
-
执行外部命令:当你需要在Python脚本中运行操作系统的命令行命令时,比如执行
ls、grep、find等。 -
系统集成:在需要与系统其他部分集成时,比如调用系统服务或与其他应用程序交互。
-
并行处理:当需要并行执行多个任务,并且这些任务可以作为独立的进程运行时。
-
资源隔离:当需要隔离任务的资源使用,比如内存和处理器时间,以避免一个任务消耗过多资源影响整个应用程序。
-
安全性:在需要安全地执行不受信的输入时,可以通过
subprocess模块来避免注入攻击。
使用subprocess的好处:
-
跨平台:
subprocess模块在Windows、Linux和macOS等多种操作系统上都可用,使得编写跨平台的脚本成为可能。 -
简单易用:通过
subprocess.run()、subprocess.Popen()等函数,可以很容易地启动和管理子进程。 -
强大的功能:
subprocess提供了多种方式来与子进程进行交互,包括标准输入/输出/错误流的重定向、管道通信等。 -
异常处理:可以捕获并处理子进程执行过程中出现的异常,比如命令未找到、执行出错等。
-
资源管理:可以控制子进程的资源使用,比如设置时间限制、内存限制等。
-
并发执行:可以并发地执行多个子进程,提高程序的效率。
-
与
asyncio集成:subprocess模块可以与asyncio模块一起使用,实现异步地执行和管理子进程。 -
细粒度控制:
subprocess提供了细粒度的控制,比如可以等待子进程完成、查询子进程的状态、终止子进程等。 -
安全性:通过避免shell=True的使用,可以防止shell注入攻击,提高脚本的安全性。
使用subprocess模块时,需要谨慎处理与外部命令的交互,避免潜在的安全风险,比如注入攻击。同时,合理利用subprocess可以极大地提高Python脚本的功能和灵活性。
2、为什么如下代码,会无限期挂起?【from example13_5.py】
import asyncio from asyncio.subprocess import Process async def main(): program = ['python', 'listing_13_4.py'] # 类似 python listing_13_4.py process: Process = await asyncio.create_subprocess_exec(*program, stdout=asyncio.subprocess.PIPE) print(f'Process pid is: {process.pid}') return_code = await process.wait() print(f'Process returned: {return_code}') asyncio.run(main())
这段代码会无限期挂起的原因是因为在子进程 (subprocess) 的 stdout 被设置为 asyncio.subprocess.PIPE 时, 如果不读取该管道中的数据, 子进程会一直保持运行状态。
当我们使用 asyncio.create_subprocess_exec 创建子进程时, 可以指定 stdout 和 stderr 参数来控制子进程的标准输出和标准错误流的行为。
如果将 stdout 或 stderr 设置为 asyncio.subprocess.PIPE, 那么子进程的相应输出流将被捕获到一个管道中,而不是被直接发送到控制台。
在本例中, 由于我们将 stdout 设置为 asyncio.subprocess.PIPE,子进程 listing_13_4.py 的标准输出将被捕获到一个管道中。然而,在代码中我们并没有读取这个管道中的数据,这就导致了死锁的发生。
具体来说,当子进程试图将数据写入管道时,如果管道已满且没有任何读取者,那么子进程将被阻塞,无法继续执行。
同时,由于父进程 (主进程) 正在等待子进程退出 (await process.wait()) ,因此父进程也被阻塞了。这就形成了一个相互等待的死锁状态,导致程序无限期挂起。
要解决这个问题,我们需要在某个时候读取管道中的数据,以便子进程可以继续执行。
一种简单的方式是使用 process.communicate() 方法来等待子进程退出,同时读取其标准输出和标准错误流。另一种更加高效的方式是使用异步读取管道数据的方法,例如 asyncio.StreamReader。
用一个图来解释这个无限期挂起的原因:
+---------------------+
| 主进程 (父进程) |
| |
| await process.wait()
| |
+-----|------+
|
| 创建子进程
|
+------------|---------------+
| v |
+----------+---------------+ +---------+----------+
| 子进程 listing_13_4.py | | 管道 (PIPE) |
| | =======> | ===========>(满) |
| | 输出数据 | | |
+-------------+---------------+----------+----------+
| |
| 阻塞(无法继续写入) | 阻塞(无任何读取者)
| |
+-------------------------+
如图所示:
- 主进程创建了子进程
listing_13_4.py, 并将子进程的stdout连接到一个管道 (PIPE) - 子进程开始向管道中写入输出数据, 当管道已满且没有读取者时, 子进程将被阻塞, 无法继续执行
- 主进程正在等待子进程退出 (
await process.wait()) - 由于子进程被阻塞, 无法退出, 所以主进程也被无限期阻塞
这就形成了一个死锁状态,导致程序无限期挂起。要解决这个问题,需要在某个时候从管道中读取数据,以允许子进程继续执行和退出。
3、如下代码的communicate的概念的理解
import asyncio from asyncio.subprocess import Process async def main(): program = ['python3', 'listing_13_4.py'] process: Process = await asyncio.create_subprocess_exec(*program, stdout=asyncio.subprocess.PIPE) print(f'Process pid is: {process.pid}') stdout, stderr = await process.communicate() # 如何理解? print(stdout) print(stderr) print(f'Process returned: {process.returncode}') asyncio.run(main())
在这段代码中,communicate 是 asyncio.subprocess.Process 类的一个方法。这个方法用于与子进程进行交互,具体来说,
就是从子进程的标准输出(stdout)和标准错误输出(stderr)中读取数据,同时也会向子进程的标准输入(stdin)发送数据(如果有的话)。
通俗解释:
await asyncio.create_subprocess_exec(*program, stdout=asyncio.subprocess.PIPE)这行代码创建了一个子进程,并将该子进程的标准输出和标准错误输出设置为管道(PIPE),这样我们就可以从这些管道中读取数据。stdout, stderr = await process.communicate()这行代码等待子进程执行完毕,并从子进程的标准输出和标准错误输出中读取数据。读取的数据分别存储在stdout和stderr变量中。print(stdout)和print(stderr)分别打印子进程的标准输出和标准错误输出。print(f'Process returned: {process.returncode}')打印子进程的退出状态码。
总的来说,communicate 方法用于与子进程进行交互,包括从子进程的标准输出和标准错误输出中读取数据,以及向子进程的标准输入发送数据。
4、python中经常提到的上下文切换的开销是什么意思?
在 Python 中,上下文切换(Context Switching)的开销是指在多任务并发执行时,操作系统或解释器在多个任务之间切换执行时所花费的时间和资源。
每个任务都有一个执行上下文,包括程序的计数器、栈、状态等。当进行上下文切换时,当前任务的状态被保存,另一个任务的状态被恢复,以便继续执行。
上下文切换开销的主要表现如下:
-
时间开销:保存和恢复上下文需要时间,尤其是在任务很多的情况下,频繁的上下文切换会导致大量的时间被用于切换,而不是实际的计算。
-
资源开销:上下文切换需要占用 CPU 资源,以及可能涉及内存的读写操作。
-
性能影响:频繁的上下文切换可能会导致程序性能下降,因为任务的执行被中断,需要一段时间后才能继续,这会影响任务的响应时间和吞吐量。
举例说明:
假设你正在运行一个 Python 程序,这个程序创建了多个线程或协程来处理不同的任务。每个线程或协程都可以视为一个单独的执行上下文。
-
当一个线程或协程正在执行时,操作系统或 Python 解释器可能会决定进行上下文切换,以便其他线程或协程也能获得 CPU 时间。
-
在切换之前,当前线程或协程的状态(如程序计数器、局部变量等)需要被保存。
-
接下来,操作系统或解释器选择另一个线程或协程,并恢复其之前保存的状态。
-
这个过程重复进行,每次切换都有一定的开销。
在现代操作系统中,上下文切换通常非常快速,但对于高并发和需要大量计算的任务,频繁的上下文切换仍然可能成为一个性能瓶颈。
为了减少上下文切换的开销,开发者可能会采取一些策略,如使用更少的线程或协程,优化算法减少并发需求,或者使用异步编程模型来避免不必要的上下文切换。

浙公网安备 33010602011771号