Chapter13_管理子进程

1、什么时候用subprocess, 用subprocess有什么好处?

subprocess模块是Python标准库的一部分,它允许你启动新的进程、连接到它们的输入/输出/错误管道,并且获取它们的返回值。以下是一些使用subprocess模块的情况和它的好处:

使用subprocess的情况:

  1. 执行外部命令:当你需要在Python脚本中运行操作系统的命令行命令时,比如执行lsgrepfind等。

  2. 系统集成:在需要与系统其他部分集成时,比如调用系统服务或与其他应用程序交互。

  3. 并行处理:当需要并行执行多个任务,并且这些任务可以作为独立的进程运行时。

  4. 资源隔离:当需要隔离任务的资源使用,比如内存和处理器时间,以避免一个任务消耗过多资源影响整个应用程序。

  5. 安全性:在需要安全地执行不受信的输入时,可以通过subprocess模块来避免注入攻击。

使用subprocess的好处:

  1. 跨平台:subprocess模块在Windows、Linux和macOS等多种操作系统上都可用,使得编写跨平台的脚本成为可能。

  2. 简单易用:通过subprocess.run()subprocess.Popen()等函数,可以很容易地启动和管理子进程。

  3. 强大的功能:subprocess提供了多种方式来与子进程进行交互,包括标准输入/输出/错误流的重定向、管道通信等。

  4. 异常处理:可以捕获并处理子进程执行过程中出现的异常,比如命令未找到、执行出错等。

  5. 资源管理:可以控制子进程的资源使用,比如设置时间限制、内存限制等。

  6. 并发执行:可以并发地执行多个子进程,提高程序的效率。

  7. asyncio集成:subprocess模块可以与asyncio模块一起使用,实现异步地执行和管理子进程。

  8. 细粒度控制:subprocess提供了细粒度的控制,比如可以等待子进程完成、查询子进程的状态、终止子进程等。

  9. 安全性:通过避免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 创建子进程时, 可以指定 stdoutstderr 参数来控制子进程的标准输出和标准错误流的行为。

如果将 stdoutstderr 设置为 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) |
|             | =======>  | ===========>(满)        |
|             |  输出数据    |            |          |
+-------------+---------------+----------+----------+
                |                         |
                | 阻塞(无法继续写入)             | 阻塞(无任何读取者)
                |                         |
                +-------------------------+

如图所示:

  1. 主进程创建了子进程 listing_13_4.py, 并将子进程的 stdout 连接到一个管道 (PIPE)
  2. 子进程开始向管道中写入输出数据, 当管道已满且没有读取者时, 子进程将被阻塞, 无法继续执行
  3. 主进程正在等待子进程退出 (await process.wait())
  4. 由于子进程被阻塞, 无法退出, 所以主进程也被无限期阻塞

这就形成了一个死锁状态,导致程序无限期挂起。要解决这个问题,需要在某个时候从管道中读取数据,以允许子进程继续执行和退出。

 

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)发送数据(如果有的话)。

通俗解释:

  1. await asyncio.create_subprocess_exec(*program, stdout=asyncio.subprocess.PIPE) 这行代码创建了一个子进程,并将该子进程的标准输出和标准错误输出设置为管道(PIPE),这样我们就可以从这些管道中读取数据。
  2. stdout, stderr = await process.communicate() 这行代码等待子进程执行完毕,并从子进程的标准输出和标准错误输出中读取数据。读取的数据分别存储在 stdout 和 stderr 变量中。
  3. print(stdout) 和 print(stderr) 分别打印子进程的标准输出和标准错误输出。
  4. print(f'Process returned: {process.returncode}') 打印子进程的退出状态码。

总的来说,communicate 方法用于与子进程进行交互,包括从子进程的标准输出和标准错误输出中读取数据,以及向子进程的标准输入发送数据。

 

4、python中经常提到的上下文切换的开销是什么意思?

在 Python 中,上下文切换(Context Switching)的开销是指在多任务并发执行时,操作系统或解释器在多个任务之间切换执行时所花费的时间和资源。

每个任务都有一个执行上下文,包括程序的计数器、栈、状态等。当进行上下文切换时,当前任务的状态被保存,另一个任务的状态被恢复,以便继续执行。

上下文切换开销的主要表现如下:

  1. 时间开销:保存和恢复上下文需要时间,尤其是在任务很多的情况下,频繁的上下文切换会导致大量的时间被用于切换,而不是实际的计算。

  2. 资源开销:上下文切换需要占用 CPU 资源,以及可能涉及内存的读写操作。

  3. 性能影响:频繁的上下文切换可能会导致程序性能下降,因为任务的执行被中断,需要一段时间后才能继续,这会影响任务的响应时间和吞吐量。

举例说明:

假设你正在运行一个 Python 程序,这个程序创建了多个线程或协程来处理不同的任务。每个线程或协程都可以视为一个单独的执行上下文。

  1. 当一个线程或协程正在执行时,操作系统或 Python 解释器可能会决定进行上下文切换,以便其他线程或协程也能获得 CPU 时间。

  2. 在切换之前,当前线程或协程的状态(如程序计数器、局部变量等)需要被保存。

  3. 接下来,操作系统或解释器选择另一个线程或协程,并恢复其之前保存的状态。

  4. 这个过程重复进行,每次切换都有一定的开销。

在现代操作系统中,上下文切换通常非常快速,但对于高并发和需要大量计算的任务,频繁的上下文切换仍然可能成为一个性能瓶颈

为了减少上下文切换的开销,开发者可能会采取一些策略,如使用更少的线程或协程,优化算法减少并发需求,或者使用异步编程模型来避免不必要的上下文切换。

 

 

 

 

 

 
 
posted @ 2024-06-05 17:11  AlphaGeek  阅读(62)  评论(0)    收藏  举报