Python 并发模式详解与子进程主函数深度解析
基本概念
IO密集型和CPU密集型
- 综述
| 维度 | I/O 密集型 (I/O-bound) | CPU 密集型 (CPU-bound) |
|---|---|---|
| 核心定义 | 程序大部分时间在等待输入/输出操作完成 | 程序大部分时间在进行计算和逻辑处理 |
| 瓶颈所在 | 磁盘读写速度、网络延迟、数据库响应 | CPU 的主频、核心数、计算能力 |
| CPU 状态 | 低利用率(经常处于空闲/等待状态) | 高利用率(接近 100%,满载运行) |
| 典型场景 | 读写文件、网络请求、数据库查询、API 调用 | 视频编码、图像渲染、复杂数学运算、加密解密 |
-
I/O 密集型任务的特点是:程序发起一个请求后,需要花费大量时间等待外部设备或远程服务响应。
- 发生了什么?
当你请求一个网页或读取一个大文件时,CPU 发出指令后,数据通过网线或硬盘磁头传输的速度相对较慢。在等待数据回来的这段时间里,CPU 是"闲置"的。 - 如何优化?
既然 CPU 在等待时很闲,我们就应该让它去处理其他任务。
并发策略:适合使用多线程或异步 I/O(如 Python 的 async/await )。 - 原理:当一个线程在等待 I/O 时,CPU 可以迅速切换到另一个线程去执行任务,从而最大化资源利用率。
- 线程池配置建议:
通常建议设置较多的线程数(例如 2倍 CPU 核心数 或更多),因为线程经常会被阻塞,需要更多线程来保持 CPU 忙碌。
- 发生了什么?
-
CPU 密集型任务的特点是:程序需要处理大量的数据计算,CPU 几乎没有停歇的时间。
- 发生了什么?
例如进行视频压缩或计算圆周率,CPU 需要不断地进行加减乘除和逻辑判断。此时,系统的瓶颈完全在于 CPU 算得够不够快。 - 如何优化?
增加线程并不一定能解决问题,因为 CPU 已经满载了。过多的线程反而会导致频繁的上下文切换,消耗额外的 CPU 资源。 - 并发策略:适合使用多进程(利用多核 CPU 并行计算)或 GPU 加速(如 CUDA)。
- 原理:将庞大的计算任务拆分成小块,分发给不同的 CPU 核心同时处理(并行计算)。
- 线程/进程池配置建议:
通常建议线程/进程数与 CPU 核心数保持一致(例如 N 或 N+1),以避免上下文切换带来的开销。
- 发生了什么?
并行和并发
简单来说,并发是看起来同时在做 ,而并行是真的同时在做。
| 维度 | 并发 (Concurrency) | 并行 (Parallelism) |
|---|---|---|
| 核心本质 | 任务切换:在一段时间内交替执行多个任务 | 同时执行:在同一时刻真正地同时执行多个任务 |
| 硬件要求 | 单核CPU即可实现,通过时间片轮转调度 | 必须依赖多核CPU或多处理器 |
| 关注点 | 结构与调度:如何高效地组织和管理任务,避免资源闲置 | 性能与加速:如何利用更多计算资源来加快任务处理速度 |
| 生活类比 | 一个人一边听音乐,一边回复消息,一边喝水 | 你和朋友一起吃饭,你们同时在吃 |
- 并发:解决"CPU利用率"问题 核心目标是不让CPU闲着。当一个任务因为等待I/O(如读取文件、网络请求)而暂停时,CPU可以立刻切换到另一个任务去执行。适用于 I/O 密集型任务。
- 并行的核心目标是让任务跑得更快。它将一个庞大的计算任务拆分成多个小任务,然后分配给多个CPU核心同时处理。适用于CPU密集型任务。
- 实际应用:
在实际的复杂系统中,并发和并行并非互斥,而是协同工作的。
一个高性能的"高并发"系统,往往是两者的结合体:- 在架构层面,它利用并发机制(如线程池、事件循环)来高效地调度和响应海量的用户请求。
- 在计算层面,对于其中某个需要大量计算的请求(比如图片处理),系统会利用并行机制(多核CPU)来加速处理,尽快返回结果。
- 总结一下:并发是一种程序设计模式,关注如何更好地安排任务;而并行是一种硬件执行状态,关注如何利用更多资源来加速任务。
概述
本文档深入讲解 Python 的并发编程模式,并结合 Xinference 子进程主函数代码进行实战分析,最后总结相关面试考点。
1. Python 并发模式详解
Python 提供了多种并发编程模式,每种模式都有其特定的使用场景和适用条件。
1.1 并发模式概览
┌─────────────────────────────────────────────────────────────────────────────┐
│ Python 并发模式全景图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 多进程 │ │ 多线程 │ │ 协程 │ │ 其他模式 │ │
│ │ (Process) │ │ (Thread) │ │ (Coroutine) │ │ (混合模式) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CPU密集型 │ │ I/O密集型 │ │ I/O密集型 │ │ 混合型 │ │
│ │ 突破GIL限制 │ │ 等待时间长 │ │ 高并发场景 │ │ Actor模型 │ │
│ │ 独立内存 │ │ 共享内存 │ │ 轻量级 │ │ 线程池+协程 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
1.2 多进程模式 (Multiprocessing)
1.2.1 核心概念
1.2.2 进程启动方式
| 启动方式 | 平台支持 | 内存状态 | CUDA支持 | 启动速度 |
|---|---|---|---|---|
fork |
Unix only | 复制父进程 | ❌ | 最快 |
spawn |
全平台 | 全新进程 | ✅ | 较慢 |
forkserver |
Unix only | 服务器fork | ❌ | 中等 |
python
import multiprocessing
# 设置启动方式
multiprocessing.set_start_method("spawn")
# 查看当前启动方式
print(multiprocessing.get_start_method())
1.2.3 使用场景
| 场景 | 是否适用 | 原因 |
|---|---|---|
| CPU 密集型计算 | ✅ | 突破 GIL 限制,真正并行 |
| CUDA/GPU 计算 | ✅ | 需要 spawn 模式独立初始化 |
| 数据处理/科学计算 | ✅ | 利用多核 CPU |
| 简单 I/O 操作 | ❌ | 进程创建开销大,不划算 |
| 需要共享大量数据 | ❌ | 进程间通信开销大 |
1.2.4 进程间通信 (IPC)
在多进程模式下,由于每个进程都拥有独立的内存空间,进程间无法直接共享数据,因此必须依赖操作系统提供的进程间通信(IPC)机制。常用的方式有:管道 (Pipe), 信号 (Signal), 消息队列 (Message Queue), 共享内存 (Shared Memory),信号量 (Semaphore), 套接字 (Socket)
1.2.5 进程池 (ProcessPool)
进程池是一种"预先创建、复用资源、限制数量"的并发管理策略。它主要用于解决频繁创建和销毁进程带来的巨大性能开销,并能有效防止系统因进程过多而崩溃。
在计算机层面,进程池的工作流程如下:
-
初始化 (Initialization):
主程序启动时,根据配置(通常是CPU核心数),预先创建好 N 个空闲的工作进程 (Worker Processes)。
-
提交任务 (Submit):
当有新任务(如处理一个文件、计算一个数学题)到来时,主进程将任务放入一个任务队列 (Task Queue) 中。
-
调度与执行 (Dispatch & Execute):
池中的空闲进程会从队列中"抢"任务来执行。
-
复用与回收 (Reuse & Recycle):
任务执行完毕后,进程不会销毁,而是回到池中继续等待下一个任务。只有当所有任务都完成且池被关闭时,这些进程才会被销毁。
1.2.6 最佳实践
python
import multiprocessing
import signal
import sys
def worker_task():
"""工作进程任务"""
def sigterm_handler(signum, frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
try:
while True:
pass
except KeyboardInterrupt:
pass
finally:
pass
def main():
multiprocessing.set_start_method("spawn")
processes = []
for i in range(4):
p = multiprocessing.Process(target=worker_task)
p.daemon = True
processes.append(p)
p.start()
try:
for p in processes:
p.join()
except KeyboardInterrupt:
pass
finally:
for p in processes:
if p.is_alive():
p.terminate()
if __name__ == "__main__":
main()
最佳实践要点:
- 设置启动方式 :在主模块开头设置
spawn模式
- 避免副作用:spawn 模式会启动一个全新的 Python 解释器进程,只导入主模块来执行子进程的代码。这避免了 fork 模式(Unix/Linux 默认)可能带来的问题。fork 会复制父进程的整个内存空间,如果父进程中有一些未释放的资源或复杂的 C 扩展库状态,子进程可能会继承一个"不干净"的状态,导致难以预料的崩溃或错误。
- 保证跨平台一致性:使用 spawn 可以确保你的程序在 Windows、macOS 和 Linux 上的行为基本一致,减少了因平台差异导致的 bug。
- 守护进程 :辅助进程设置
daemon=True
- 守护进程(Daemon Process)是一个在后台运行的进程,它的生命周期依赖于主进程。
- 防止程序无法正常退出:当一个 Python 程序结束时,它会等待所有非守护进程的子进程执行完毕。如果你的某个子进程因为 bug 进入了死循环,主进程就会一直卡住,无法正常退出。
- 自动清理:将那些不重要的、辅助性的后台任务(如心跳检测、日志写入)设置为守护进程。当主进程结束时,这些守护进程会被立即强制终止,从而保证主程序能够顺利退出。
- 注意:守护进程不应执行关键任务,因为它可能在任何时刻被强制中断,没有机会执行清理代码。
- 信号处理:子进程注册信号处理器实现优雅退出
- 当在终端按下 Ctrl+C 或使用 kill 命令时,操作系统会向进程发送一个信号(如 SIGINT 或 SIGTERM)。信号处理就是让你的程序能够"捕获"这些信号,并做出相应的反应。
- 通过注册一个信号处理器,你可以在进程退出前执行一些必要的清理操作,例如:保存进度、关闭网络连接、释放锁等。
- 资源清理 :使用
finally确保清理
- 无论程序是正常结束还是因为异常而中断,finally 代码块中的代码都一定会被执行。
- 保证资源释放:在多进程环境中,进程可能会因为各种意外情况(如未捕获的异常、外部信号)而终止。将资源清理代码(如 file.close(), connection.close())放在 finally 块中,可以确保无论发生什么,这些宝贵的系统资源都能被正确释放,避免资源泄漏。
- 避免全局状态:进程间不共享全局变量
- 每个进程都有自己独立的内存空间。这意味着在一个进程中修改一个全局变量,不会影响到其他进程中的同名变量。
- 如果确实需要在进程间共享数据,必须使用专门的进程间通信(IPC)机制,例如 multiprocessing.Queue、Pipe 或 Value/Array(共享内存)。这迫使开发者采用更明确、更安全的通信方式。
1.3 多线程模式 (Threading)
1.3.1 核心概念
1.3.2 GIL (Global Interpreter Lock)
GIL(全局解释器锁)是 Python(特指 CPython 解释器)为了简化内存管理而引入的一把"全局互斥锁"。它导致在同一时刻,无论你的 CPU 有多少个核心,Python 进程只能有一个线程在 CPU 上执行字节码。
- 本质:是 CPython 解释器(你从 Python 官网下载的标准版本)中的一把互斥锁。
- 作用:保护 Python 对象的内存管理(主要是引用计数),防止多线程同时修改内存导致数据错乱。
- 后果:
- CPU 密集型任务(如复杂的数学计算、图像处理):多线程不仅不能加速,反而因为线程频繁切换争夺锁,导致运行速度比单线程还慢。
- I/O 密集型任务(如爬虫、文件读写):影响不大。因为线程在等待 I/O 时会自动释放 GIL,让其他线程有机会执行。
既然 threading 模块受限于 GIL,我们需要使用其他手段来利用多核 CPU。以下是 几 种主流的实战解法:
| 方案 | 核心原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 多进程 | 多个进程,每个进程一个 GIL | CPU 密集型 (计算、加密) | 真正利用多核,代码改动小 | 内存占用高,进程间通信复杂 |
| C 扩展 | C 语言层释放 GIL | 高性能计算 | 性能极致,NumPy 等库已采用 | 开发门槛高,需编写 C/C++ |
| 协程 | 单线程事件循环 | I/O 密集型 (高并发网络) | 极高的并发量,资源消耗极低 | 无法利用多核计算,代码需异步化 |
| 无 GIL 版 | 移除解释器锁 | 通用 (未来趋势 python3.13) | 原生多线程并行,兼容现有代码 | 目前处于实验阶段,生态尚不成熟 |
┌─────────────────────────────────────────────────────────────────┐
│ GIL 工作原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Python 解释器进程 │ │
│ │ │ │
│ │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ │
│ │ │Thread1│ │Thread2│ │Thread3│ │Thread4│ │ │
│ │ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ │
│ │ │ │ │ │ │ │
│ │ └───────────┴─────┬─────┴───────────┘ │ │
│ │ │ │ │
│ │ ┌────▼────┐ │ │
│ │ │ GIL │ │ │
│ │ │ (全局锁) │ │ │
│ │ └────┬────┘ │ │
│ │ │ │ │
│ │ ┌────▼────┐ │ │
│ │ │ CPU核心 │ │ │
│ │ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 同一时刻只有一个线程能获取 GIL,执行 Python 字节码 │
│ │
└─────────────────────────────────────────────────────────────────┘
GIL 的影响:
python
import threading
import time
def cpu_bound_task():
"""CPU 密集型任务"""
count = 0
for i in range(10000000):
count += i
def io_bound_task():
"""I/O 密集型任务"""
time.sleep(0.1)
# CPU 密集型 - 多线程反而变慢
start = time.time()
threads = [threading.Thread(target=cpu_bound_task) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"多线程 CPU 任务: {time.time() - start:.2f}s")
start = time.time()
for _ in range(4):
cpu_bound_task()
print(f"单线程 CPU 任务: {time.time() - start:.2f}s")
# I/O 密集型 - 多线程显著加速
start = time.time()
threads = [threading.Thread(target=io_bound_task) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"多线程 I/O 任务: {time.time() - start:.2f}s")
start = time.time()
for _ in range(10):
io_bound_task()
print(f"单线程 I/O 任务: {time.time() - start:.2f}s")
在 Python 的多线程编程中,线程同步和线程池是两个至关重要的高级概念。前者解决了多线程环境下的数据安全问题,后者则提供了一种高效管理线程生命周期的方法。
1.3.3 线程同步
当多个线程同时访问和修改同一个共享资源(例如一个全局变量、一个文件)时,如果没有协调机制,就会发生竞态条件 (Race Condition),导致数据错乱或不一致。
线程同步的目的就是通过特定的机制,确保在任一时刻,只有一个(或有限个)线程能够访问共享资源,从而保证数据的一致性和正确性。
Python 的 threading 模块提供了多种同步工具,最常用的是锁 (Lock) 和队列 (Queue)。
锁是最基础的同步原语,它就像一把钥匙,只有一个线程能拿到钥匙进入"临界区"(访问共享资源的代码段),其他线程必须等待。
python
import threading
# 1. Lock - 互斥锁
lock = threading.Lock()
counter = 0
def increment():
global counter
for _ in range(100000):
with lock:
counter += 1
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # 1000000
1.3.4 线程池
为什么需要线程池?
手动使用 threading.Thread 创建和管理大量线程存在两个主要问题:
- 性能开销大:创建和销毁线程是昂贵的操作,会消耗系统资源。
- 资源失控:如果不加限制地创建线程,可能会耗尽系统内存或 CPU 资源,导致程序崩溃。
线程池通过复用一组预先创建好的线程来解决这些问题。当你需要执行任务时,只需将任务提交给线程池,池会分配一个空闲线程来处理。任务完成后,线程不会被销毁,而是回到池中等待下一个任务。
1.3.5 使用场景
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 网络 I/O 操作 | ✅ | 等待期间释放 GIL |
| 文件 I/O 操作 | ✅ | 等待期间释放 GIL |
| GUI 应用程序 | ✅ | 保持界面响应 |
| CPU 密集型计算 | ❌ | GIL 限制,无法并行 |
| 需要真正并行 | ❌ | 受 GIL 限制 |
1.3.6 最佳实践
最佳实践要点:
-
避免 CPU 密集型任务:多线程不适合 CPU 密集型
-
使用线程池:避免频繁创建销毁线程
-
设置超时:网络请求设置超时,避免阻塞
-
异常处理:每个任务独立处理异常
子线程里的报错不会自动"传染"给主线程,你必须显式地去捕获或检查它,否则程序可能会在"看似正常"的情况下丢失数据或静默失败。
在 Python 的 threading 模块中,如果子线程抛出了异常且没有在内部捕获,默认行为是打印错误堆栈到 stderr,然后该线程静默退出。主线程通常感知不到子线程已经挂了,它会继续运行,仿佛什么都没发生。
-
限制线程数:根据 I/O 等待时间设置合理线程数
线程池的大小不是拍脑袋决定的(比如随便写个 100),而是要根据"CPU 计算时间"和"I/O 等待时间"的比例来科学计算,以达到资源利用率的最大化。
为什么要根据 I/O 时间设置?
- 如果线程数太少:当所有线程都在等待 I/O(比如都在等数据库返回)时,CPU 就会空闲下来,浪费了计算资源。
- 如果线程数太多:虽然 CPU 不空闲了,但过多的线程会导致频繁的上下文切换(CPU 在线程间切换需要消耗时间),反而降低性能,甚至导致内存溢出。
1.4 协程模式 (Coroutine/asyncio)
1.4.1 核心概念
一种用户态的、轻量级的、协作式的并发编程模型。允许在单个线程内高效地管理多个执行流,通过主动"挂起"和"恢复"来避免阻塞,从而在处理大量 I/O 密集型任务时表现出极高的性能。
由用户态的运行时(如 Python 的 asyncio 库)管理。创建和切换只涉及用户态的少量指令,开销极低。每个协程的栈可以小到 KB 甚至几十字节。因此,单个线程可以轻松承载数十万甚至上百万个协程。
你可以将协程模式想象成一个高效的单线程厨师:
他同时处理多个订单(协程)。当一个订单需要等待烤箱(I/O 操作)时,他不会傻站着,而是立刻挂起这个订单的处理,转而去准备另一个订单的食材(恢复并执行另一个协程)。整个过程由他自己(事件循环)高效调度,无需其他厨师(线程)帮忙,就能实现惊人的并发处理能力。
因此,协程模式是解决高并发 I/O 密集型任务(如 Web 服务器、网络爬虫、数据库驱动)的理想选择。
核心机制:
- 挂起与恢复
这是协程实现并发的核心机制。
- 挂起 (Suspend):协程在执行到 await 关键字时,会暂停自己的执行,保存当前的执行上下文(如局部变量、指令指针),并将控制权交还给事件循环。
- 恢复 (Resume):当 await 的异步操作(如网络响应到达)完成后,事件循环会重新调度该协程,从它上次挂起的地方继续执行。
这个过程对开发者来说是透明的,代码看起来是同步的、线性的,但底层却是非阻塞的异步执行。
- 事件循环
事件循环是协程的"大脑"和调度中心。它在一个单线程中持续运行,主要负责:
- 维护任务队列:管理所有待执行的协程。
- 监听 I/O 事件:通过操作系统提供的机制(如 epoll, kqueue)监听网络、文件等 I/O 事件是否就绪。
- 调度协程:当一个协程因 await 而挂起时,事件循环会立即切换到队列中下一个就绪的协程去执行,确保 CPU 永不空闲。
python
import asyncio
async def hello():
"""异步函数"""
print("Hello")
await asyncio.sleep(1)
print("World")
# 运行协程
asyncio.run(hello())
# 或者
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
1.4.2 协程 vs 线程 vs 进程
┌─────────────────────────────────────────────────────────────────────────────┐
│ 并发模式对比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 进程 (Process) │ │
│ │ 内存: 独立地址空间 (~10MB+ 启动开销) │ │
│ │ 切换: 操作系统调度,开销大 │ │
│ │ 通信: Queue/Pipe/共享内存,需要序列化 │ │
│ │ 并行: 真正并行,突破 GIL │ │
│ │ 数量: 通常几个到几十个 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 线程 (Thread) │ │
│ │ 内存: 共享地址空间 (~8KB 栈空间) │ │
│ │ 切换: 操作系统调度,开销中等 │ │
│ │ 通信: 共享变量,需要同步原语 │ │
│ │ 并行: 受 GIL 限制,同一时刻只有一个线程执行 │ │
│ │ 数量: 通常几十到几百个 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 协程 (Coroutine) │ │
│ │ 内存: 单线程,~1KB 协程对象 │ │
│ │ 切换: 用户态切换,开销极小 │ │
│ │ 通信: 共享变量,无需同步 │ │
│ │ 并发: 单线程并发,适合 I/O 密集型 │ │
│ │ 数量: 可达数万甚至数十万 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
1.4.3 事件循环 (Event Loop)
python
import asyncio
async def task1():
print("Task 1 start")
await asyncio.sleep(1)
print("Task 1 end")
async def task2():
print("Task 2 start")
await asyncio.sleep(0.5)
print("Task 2 end")
async def main():
# 并发执行两个任务
await asyncio.gather(task1(), task2())
asyncio.run(main())
# 输出:
# Task 1 start
# Task 2 start
# Task 2 end (0.5秒后)
# Task 1 end (1秒后)
事件循环工作原理:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 事件循环详细工作流程 (asyncio) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 事件循环 (Event Loop) │ │
│ │ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ 1. 就绪队列 │ [_ready: 回调函数, 已唤醒的协程] │ │
│ │ │ (_ready) │ │ │
│ │ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ 批量执行就绪任务 │ ──▶ 执行到 await 则挂起并注册I/O监听 │ │
│ │ │ (清空队列) │ ──▶ 产生新回调则放入 [_ready] │ │
│ │ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ▼ (队列为空时) │ │
│ │ ┌──────────────────┐ │ │
│ │ │ 2. 定时任务 │ [_scheduled: sleep, call_later] │ │
│ │ │ 检查队列 │ ──▶ 若到期则移入 [_ready] │ │
│ │ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ▼ (无到期任务时) │ │
│ │ ┌──────────────────┐ │ │
│ │ │ 3. I/O 多路复用 │ epoll/kqueue/IOCP │ │
│ │ │ 等待事件就绪 │ ──▶ 设置超时时间 (最近定时器) │ │
│ │ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ 事件就绪处理 │ ──▶ I/O事件: 回调移入 [_ready] │ │
│ │ │ │ ──▶ 超时: 检查 [_scheduled] │ │
│ │ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ └──────────────────┐ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ 循环继续 │ ──▶ 返回步骤 1 │ │
│ │ └──────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
1.4.4 常用 API
python
import asyncio
async def demo_asyncio_api():
# 1. 创建任务
task = asyncio.create_task(some_coroutine())
# 2. 等待多个任务
results = await asyncio.gather(
coro1(),
coro2(),
coro3()
)
# 3. 等待第一个完成
done, pending = await asyncio.wait(
[coro1(), coro2(), coro3()],
return_when=asyncio.FIRST_COMPLETED
)
# 4. 超时控制
try:
result = await asyncio.wait_for(some_coroutine(), timeout=5.0)
except asyncio.TimeoutError:
print("Timeout!")
# 5. 屏障
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(coro1())
task2 = tg.create_task(coro2())
# 6. 队列
queue = asyncio.Queue()
await queue.put("item")
item = await queue.get()
# 7. 锁
lock = asyncio.Lock()
async with lock:
pass
# 8. 事件
event = asyncio.Event()
await event.wait()
event.set()
# 9. 信号量
semaphore = asyncio.Semaphore(10)
async with semaphore:
pass
async def some_coroutine():
await asyncio.sleep(0.1)
return "result"
async def coro1():
return 1
async def coro2():
return 2
async def coro3():
return 3
asyncio.run(demo_asyncio_api())
1.4.5 使用场景
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 高并发网络服务 | ✅ | 轻量级,可处理大量连接 |
| 爬虫/HTTP 客户端 | ✅ | 高效处理大量请求 |
| WebSocket 服务 | ✅ | 长连接,事件驱动 |
| 数据库操作 | ✅ | 等待 I/O,适合异步 |
| CPU 密集型计算 | ❌ | 单线程,无法利用多核 |
| 需要真正并行 | ❌ | 协程是并发,不是并行 |
1.4.6 最佳实践
python
import asyncio
import aiohttp
async def fetch_url(session, url, semaphore):
"""使用信号量限制并发数"""
async with semaphore:
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as response:
return await response.text()
except asyncio.TimeoutError:
return None
except Exception as e:
return None
async def fetch_all(urls, max_concurrent=10):
"""并发获取所有 URL"""
semaphore = asyncio.Semaphore(max_concurrent)
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
async def graceful_shutdown():
"""优雅关闭示例"""
tasks = [asyncio.create_task(some_work(i)) for i in range(10)]
try:
await asyncio.gather(*tasks)
except asyncio.CancelledError:
print("Tasks cancelled, cleaning up...")
for task in tasks:
if not task.done():
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
async def some_work(n):
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
print(f"Task {n} cancelled, doing cleanup...")
raise
if __name__ == "__main__":
asyncio.run(graceful_shutdown())
最佳实践要点:
- 使用信号量限制并发:避免资源耗尽
- 设置超时 :使用
wait_for防止无限等待 - 异常处理 :
gather(return_exceptions=True)捕获异常 - 优雅关闭 :处理
CancelledError,清理资源 - 使用 TaskGroup:Python 3.11+ 推荐使用 TaskGroup
1.5 其他并发模式
1.5.1 Actor 模型
1.5.2 线程池 + 协程混合模式
1.5.3 多进程 + 协程混合模式
1.6 并发模式选择指南
┌─────────────────────────────────────────────────────────────────────────────┐
│ 并发模式选择决策树 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 任务类型是什么? │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ CPU密集型 I/O密集型 混合型 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────┐ ┌───────────────────┐ │
│ │ 多进程模式 │ │ 协程优先 │ │ 混合模式 │ │
│ │ ProcessPool │ │ asyncio │ │ 进程池 + 协程 │ │
│ └───────────────┘ └───────────┘ └───────────────────┘ │
│ │
│ 是否需要使用 CUDA? │
│ │ │
│ ┌────────────┴────────────┐ │
│ ▼ ▼ │
│ 是 否 │
│ │ │ │
│ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ spawn 模式 │ │ 根据需求选择 │ │
│ │ 独立进程 │ │ fork/spawn │ │
│ └───────────────┘ └───────────────┘ │
│ │
│ 并发数量级? │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ < 100 100-10000 > 10000 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ 线程池 │ │ 协程/异步 │ │ 协程 + 分布式 │ │
│ │ ProcessPool │ │ asyncio │ │ Actor 模型 │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2. xinference框架 supervisor进程主函数代码详解
2.1 完整代码
python
def run(address: str, logging_conf: Optional[Dict] = None):
"""子进程主函数:启动 Supervisor"""
# 1. 设置信号处理器
def sigterm_handler(signum, frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
# 2. 获取事件循环
loop = asyncio.get_event_loop()
# 3. 创建异步任务
task = loop.create_task(
_start_supervisor(address=address, logging_conf=logging_conf)
)
# 4. 运行事件循环
loop.run_until_complete(task)
2.2 架构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 子进程启动流程架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 主进程 │ │
│ │ │ │
│ │ multiprocessing.Process(target=run, args=(address, ...)) │ │
│ │ p.daemon = True │ │
│ │ p.start() ──────────────────────────────────────────────┐ │ │
│ │ │ │ │
│ └───────────────────────────────────────────────────────────┼────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 子进程 │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ run() 函数执行 │ │ │
│ │ │ │ │ │
│ │ │ 1. signal.signal(SIGTERM, sigterm_handler) │ │ │
│ │ │ └─ 注册优雅退出处理器 │ │ │
│ │ │ │ │ │
│ │ │ 2. loop = asyncio.get_event_loop() │ │ │
│ │ │ └─ 获取/创建事件循环 │ │ │
│ │ │ │ │ │
│ │ │ 3. task = loop.create_task(_start_supervisor(...)) │ │ │
│ │ │ └─ 创建异步任务 │ │ │
│ │ │ │ │ │
│ │ │ 4. loop.run_until_complete(task) │ │ │
│ │ │ └─ 阻塞运行直到任务完成 │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ _start_supervisor() 协程执行 │ │ │
│ │ │ │ │ │
│ │ │ ├─ 创建 Actor Pool │ │ │
│ │ │ ├─ 创建 Supervisor Actor │ │ │
│ │ │ ├─ 启动 Worker 组件 │ │ │
│ │ │ ├─ 发送就绪信号 (通过 Pipe) │ │ │
│ │ │ └─ await pool.join() ── 无限等待 │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 终止信号流程: │
│ │
│ 主进程调用 p.kill() ──▶ OS发送SIGTERM ──▶ sigterm_handler │
│ └──▶ sys.exit(0) ──▶ 触发清理 ──▶ 进程退出 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.3 代码分步解析
2.3.1 步骤一:设置信号处理器
python
def sigterm_handler(signum, frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
详解:
-
信号处理器定义:
pythondef sigterm_handler(signum, frame): sys.exit(0)signum:信号编号,SIGTERM = 15frame:当前的栈帧对象,包含调用栈信息sys.exit(0):触发 Python 的退出机制
-
注册信号处理器:
pythonsignal.signal(signal.SIGTERM, sigterm_handler)- 将 SIGTERM 信号与自定义处理器绑定
- 当进程收到 SIGTERM 信号时,调用
sigterm_handler
为什么需要自定义信号处理器?
| 信号 | 触发方式 | 默认行为 |
|---|---|---|
| SIGINT | Ctrl + C | 抛出 KeyboardInterrupt 异常 |
| SIGTERM | kill <pid> |
终止进程(可被捕获) |
┌─────────────────────────────────────────────────────────────────────────────┐
│ 默认行为 vs 自定义处理器 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 默认行为(没有自定义处理器): │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 收到 SIGTERM │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 操作系统立即终止进程 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 不执行 finally 块 │ │
│ │ 不关闭文件描述符 │ │
│ │ 不释放 GPU 资源 │ │
│ │ 可能导致数据损坏 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 自定义处理器: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 收到 SIGTERM │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 调用 sigterm_handler(signum, frame) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 调用 sys.exit(0) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 触发 SystemExit 异常 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 执行 finally 块 ── 清理资源 │ │
│ │ 执行上下文管理器 __exit__ │ │
│ │ 关闭文件描述符 │ │
│ │ 释放 GPU 资源 │ │
│ │ 数据完整性得到保证 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
信号处理器的执行时机:
python
import signal
import sys
import time
def handler(signum, frame):
print(f"Signal {signum} received")
sys.exit(0)
signal.signal(signal.SIGTERM, handler)
try:
print("Running...")
while True:
time.sleep(1)
except SystemExit:
print("SystemExit caught, cleaning up...")
finally:
print("Finally block executed")
当执行这段代码并按下 Ctrl + C 时,实际打印输出为:
sh
Running...
^CFinally block executed
执行步骤为:
- ✅ 打印 "Running..."
- ⏸️ 程序进入 time.sleep(1) 循环
- ⌨️ 用户按下 Ctrl + C
- 🔔 操作系统发送 SIGINT 信号
- ❌ 代码没有为 SIGINT 注册处理函数
- 🐍 Python 将 SIGINT 转换为 KeyboardInterrupt 异常
- ❌ except SystemExit 无法捕获 KeyboardInterrupt
- ✅ finally 块总是执行,打印 "Finally block executed"
- 🚫 KeyboardInterrupt 未被捕获,程序终止
在另一个终端执行 kill # 发送 SIGTERM 时
sh
# 输出:
Running...
Signal 15 received
SystemExit caught, cleaning up...
Finally block executed
2.3.2 步骤二:获取事件循环
python
loop = asyncio.get_event_loop()
详解:
-
get_event_loop() 的行为:
- get_event_loop() 在主线程首次调用时会自动创建循环
- 每个线程只能有一个事件循环, 如果当前线程已有事件循环,返回它
- 如果没有,创建并返回新的事件循环
- 主线程默认没有事件循环,需要手动创建
-
事件循环的生命周期:
┌─────────────────────────────────────────────────────────────────┐ │ 事件循环生命周期 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ loop = asyncio.get_event_loop() │ │ │ │ │ ▼ │ │ 创建/获取事件循环 │ │ │ │ │ ▼ │ │ loop.run_until_complete(task) │ │ │ │ │ ▼ │ │ 事件循环开始运行 │ │ │ │ │ ├─ 调度任务 │ │ ├─ 处理 I/O 事件 │ │ ├─ 执行回调 │ │ │ │ │ ▼ │ │ 任务完成 │ │ │ │ │ ▼ │ │ 事件循环停止 │ │ │ │ │ ▼ │ │ 返回任务结果 │ │ │ └─────────────────────────────────────────────────────────────────┘
为什么在子进程中创建事件循环?
- 进程隔离:每个进程有独立的内存空间,需要独立的事件循环
- spawn 模式:子进程启动全新的 Python 解释器,不继承父进程的事件循环
- 异步架构:Xinference 使用 asyncio 构建,需要事件循环支持
2.3.3 步骤三:创建异步任务
python
task = loop.create_task(
_start_supervisor(address=address, logging_conf=logging_conf)
)
详解:
-
create_task() 的作用:
pythontask = loop.create_task(coro)- 将协程对象封装为 Task 对象
- 将任务添加到事件循环的待执行队列
- 任务立即开始调度(不需要等待 run_until_complete)
-
协程 vs 任务:
特性 协程(Coroutine) 任务(Task) 定义 用 async def定义的异步函数由事件循环调度的协程包装对象 本质 可暂停和恢复的函数 协程的调度单元 创建方式 async def func()asyncio.create_task(coro)是否自动调度 ❌ 需要手动调度 ✅ 自动加入事件循环 状态管理 需要外部管理 内置状态管理 取消能力 ❌ 无法直接取消 ✅ 可以取消 结果获取 需要 await可以通过 .result()获取python# 协程 (Coroutine) async def my_coroutine(): await asyncio.sleep(1) return "done" coro = my_coroutine() # 创建协程对象,未调度 # 任务 (Task) task = loop.create_task(coro) # 创建并调度任务 # task 立即进入事件循环的任务队列 -
多任务并发:
python# 创建多个任务实现并发 task1 = loop.create_task(coro1()) task2 = loop.create_task(coro2()) task3 = loop.create_task(coro3()) # 并发执行 loop.run_until_complete(asyncio.gather(task1, task2, task3)) -
任务的状态管理:
pythonimport asyncio async def my_task(): await asyncio.sleep(1) return "completed" async def main(): task = asyncio.create_task(my_task()) print(task.done()) # False - 未完成 print(task.cancelled()) # False - 未取消 result = await task print(task.done()) # True - 已完成 print(task.result()) # "completed" asyncio.run(main())
2.3.4 步骤四:运行事件循环
python
loop.run_until_complete(task)
详解:
-
run_until_complete() 的作用:
- 启动事件循环
- 运行直到指定的任务完成
- 返回任务的结果
- 自动停止事件循环
-
执行流程:
┌─────────────────────────────────────────────────────────────────┐ │ run_until_complete() 执行流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ loop.run_until_complete(task) │ │ │ │ │ ▼ │ │ 启动事件循环 │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 事件循环迭代 │ │ │ │ │ │ │ │ while not task.done(): │ │ │ │ 1. 检查就绪的任务 │ │ │ │ 2. 执行任务到 await │ │ │ │ 3. 检查 I/O 事件 (selector) │ │ │ │ 4. 处理定时器 │ │ │ │ 5. 唤醒等待的协程 │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ task 完成 │ │ │ │ │ ▼ │ │ 停止事件循环 │ │ │ │ │ ▼ │ │ 返回 task.result() │ │ │ └─────────────────────────────────────────────────────────────────┘ -
阻塞特性:
python# run_until_complete 是阻塞调用 print("Before run_until_complete") loop.run_until_complete(task) # 阻塞在这里 print("After run_until_complete") # task 完成后执行 -
与 asyncio.run() 的对比:
python# 方式 1: asyncio.run() (Python 3.7+) async def main(): await task() asyncio.run(main()) # 方式 2: get_event_loop() + run_until_complete() loop = asyncio.get_event_loop() loop.run_until_complete(task()) # 区别: # asyncio.run() - 自动创建和关闭事件循环,适合主程序入口 # run_until_complete() - 手动管理事件循环,适合在已有循环中使用
2.4 完整执行时序图
时间轴
│
│ 主进程 子进程
│ │ │
│ ├─ multiprocessing.Process(target=run) │
│ ├─ p.start() ──────────────────────────▶│
│ │ │
│ │ ├─ Python 解释器初始化
│ │ ├─ 导入模块
│ │ ├─ 反序列化参数
│ │ │
│ │ ├─ run() 函数开始执行
│ │ │ │
│ │ │ ├─ signal.signal(SIGTERM, handler)
│ │ │ │ └─ 注册信号处理器
│ │ │ │
│ │ │ ├─ loop = asyncio.get_event_loop()
│ │ │ │ └─ 获取/创建事件循环
│ │ │ │
│ │ │ ├─ task = loop.create_task(...)
│ │ │ │ └─ 创建异步任务
│ │ │ │
│ │ │ └─ loop.run_until_complete(task)
│ │ │ │
│ │ │ ▼
│ │ │ ┌──────────────────────┐
│ │ │ │ _start_supervisor() │
│ │ │ │ ├─ 创建 Actor Pool │
│ │ │ │ ├─ 创建 Supervisor │
│ │ │ │ ├─ 启动 Worker │
│ │ │ │ ├─ 发送就绪信号 ────┼───▶ conn.send(READY)
│ │ │ │ └─ await pool.join()│
│ │ │ │ (无限等待) │
│ │ │ └──────────────────────┘
│ │ │
│ ├─ conn.recv() ◀─────────────────────────┤ 接收就绪信号
│ │ │
│ ├─ health_check() │
│ │ │
│ ├─ restful_api.run() ────(阻塞) │
│ │ │
│ │ ... 服务运行中 ... │
│ │ │
│ │ │
│ ├─ 用户按 Ctrl+C 或 kill 命令 │
│ │ │
│ ├─ p.kill() ────────────────────────────▶│
│ │ │
│ │ ├─ OS 发送 SIGTERM 信号
│ │ │
│ │ ├─ sigterm_handler 被调用
│ │ │ │
│ │ │ └─ sys.exit(0)
│ │ │ │
│ │ │ ▼
│ │ │ SystemExit 异常
│ │ │ │
│ │ │ ▼
│ │ │ finally 块执行
│ │ │ │
│ │ │ ▼
│ │ │ 清理资源
│ │ │ │
│ │ │ ▼
│ │ ├─ 进程退出
│ │ │
│ ├─ p.join() ◀────────────────────────────┤
│ │
│ └─ 主进程退出
│
▼
2.5 设计要点总结
| 设计要点 | 代码实现 | 作用 |
|---|---|---|
| 优雅退出 | signal.signal(SIGTERM, handler) |
确保 SIGTERM 触发清理操作 |
| 事件循环 | asyncio.get_event_loop() |
提供异步执行环境 |
| 任务调度 | loop.create_task() |
将协程封装为可调度任务 |
| 阻塞等待 | run_until_complete() |
运行事件循环直到任务完成 |
| 进程隔离 | spawn 模式 | 独立的 CUDA 和内存环境 |
3. 面试考点
3.1 基础概念题
考点 1: Python 并发模式的区别
面试题:请简述 Python 多进程、多线程、协程的区别及各自适用场景。
参考答案:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 并发模式对比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 维度 多进程 多线程 协程 │
│ ──────────────────────────────────────────────────────────────────────── │
│ 内存空间 独立 共享 单线程共享 │
│ GIL 影响 无 受限 无 │
│ 创建开销 大 (~10MB) 中 (~8KB) 小 (~1KB) │
│ 切换开销 大(系统调度) 中(系统调度) 小(用户态) │
│ 通信方式 Queue/Pipe 共享变量+锁 共享变量 │
│ 真正并行 是 否(GIL) 否 │
│ 适用场景 CPU密集型 I/O密集型 高并发I/O │
│ 最佳数量 几个~几十 几十~几百 数万~数十万 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
选择建议:
1. CPU 密集型任务 → 多进程 (ProcessPoolExecutor)
2. 少量 I/O 并发 → 多线程 (ThreadPoolExecutor)
3. 大量 I/O 并发 → 协程 (asyncio)
4. 需要使用 CUDA → 多进程 spawn 模式
考点 2: GIL 的作用和影响
面试题:什么是 GIL?它对 Python 多线程有什么影响?如何规避?
参考答案:
python
# GIL (Global Interpreter Lock) 是 CPython 的全局解释器锁
# 作用:
# 1. 保证同一时刻只有一个线程执行 Python 字节码
# 2. 简化 CPython 的内存管理(引用计数)
# 3. 保护 C 扩展的线程安全
# 影响:
# 1. 多线程无法利用多核 CPU 进行 Python 代码的并行计算
# 2. I/O 操作会释放 GIL,所以多线程适合 I/O 密集型
# 规避方法:
# 1. 使用多进程 (multiprocessing)
# 2. 使用 C 扩展释放 GIL (numpy, pandas 等)
# 3. 使用其他 Python 实现 (Jython, IronPython)
# 4. 使用协程 (asyncio) 处理高并发 I/O
# 示例:GIL 对 CPU 密集型任务的影响
import threading
import time
def cpu_task():
total = 0
for i in range(10000000):
total += i
# 单线程
start = time.time()
cpu_task()
cpu_task()
print(f"单线程: {time.time() - start:.2f}s")
# 多线程(不会加速,反而可能变慢)
start = time.time()
t1 = threading.Thread(target=cpu_task)
t2 = threading.Thread(target=cpu_task)
t1.start(); t2.start()
t1.join(); t2.join()
print(f"多线程: {time.time() - start:.2f}s")
# 多进程(会加速)
from multiprocessing import Process
start = time.time()
p1 = Process(target=cpu_task)
p2 = Process(target=cpu_task)
p1.start(); p2.start()
p1.join(); p2.join()
print(f"多进程: {time.time() - start:.2f}s")
考点 3: 进程启动方式
面试题:Python multiprocessing 的三种启动方式是什么?各有什么特点?
参考答案:
python
# 三种启动方式:fork, spawn, forkserver
# ┌─────────────────────────────────────────────────────────────────────────┐
# │ 进程启动方式对比 │
# ├─────────────────────────────────────────────────────────────────────────┤
# │ │
# │ 方式 fork spawn forkserver │
# │ ──────────────────────────────────────────────────────────────────── │
# │ 平台 Unix only 全平台 Unix only │
# │ 内存 复制父进程 全新进程 从服务器fork │
# │ 速度 最快 较慢 中等 │
# │ 安全性 低(可能死锁) 高 高 │
# │ CUDA 不支持 支持 不支持 │
# │ 线程 不安全 安全 安全 │
# │ │
# └─────────────────────────────────────────────────────────────────────────┘
import multiprocessing
# 设置启动方式
multiprocessing.set_start_method("spawn")
# fork 模式的问题示例
import os
def child():
print(f"Child PID: {os.getpid()}")
# fork 会复制父进程的所有状态(包括锁)
# 如果父进程有线程持有锁,fork 后可能导致死锁
# spawn 模式
# 启动全新的 Python 解释器,最安全
# 必须确保主模块有 if __name__ == "__main__": 保护
# 使用场景:
# 1. 使用 CUDA/GPU → 必须用 spawn
# 2. 跨平台兼容 → 必须用 spawn
# 3. 需要高性能且不用 CUDA → fork 或 forkserver
3.2 进阶概念题
考点 4: 事件循环原理
面试题:请解释 asyncio 事件循环的工作原理。
参考答案:
python
# 事件循环 (Event Loop) 是 asyncio 的核心
# 工作原理:
# 1. 维护任务队列和 I/O 多路复用器
# 2. 循环执行:检查任务 → 执行到 await → 检查 I/O → 唤醒协程
# 3. 单线程,通过协作式调度实现并发
import asyncio
async def demo_event_loop():
# 事件循环内部伪代码
"""
while True:
1. 检查就绪的任务
2. 执行任务到下一个 await
3. 检查 I/O 事件 (使用 epoll/kqueue/IOCP)
4. 处理定时器回调
5. 唤醒等待的协程
"""
# 示例:事件循环如何调度多个任务
async def task_a():
print("A1")
await asyncio.sleep(0.1) # 交出控制权
print("A2")
async def task_b():
print("B1")
await asyncio.sleep(0.1) # 交出控制权
print("B2")
# 并发执行
await asyncio.gather(task_a(), task_b())
# 输出: A1, B1, A2, B2 (A1和B1顺序可能不同)
# 事件循环的关键 API
loop = asyncio.get_event_loop()
# 1. run_until_complete - 运行直到任务完成
loop.run_until_complete(some_task())
# 2. run_forever - 永久运行
# loop.run_forever()
# 3. call_soon - 添加回调
loop.call_soon(callback, arg)
# 4. call_later - 延迟回调
loop.call_later(1.0, callback, arg)
# 5. create_task - 创建任务
task = loop.create_task(coro())
考点 5: 协程与生成器的关系
面试题:协程和生成器有什么关系?yield 和 await 有什么区别?
参考答案:
python
# 协程的发展历史:
# Python 2.x: 基于生成器的协程 (yield)
# Python 3.4: asyncio 引入 @asyncio.coroutine
# Python 3.5: async/await 语法
# Python 3.7: async def 成为推荐方式
# 生成器 (Generator)
def my_generator():
yield 1
yield 2
yield 3
gen = my_generator()
print(next(gen)) # 1
print(next(gen)) # 2
# 基于生成器的协程 (旧式)
def old_coroutine():
result = yield "ready"
print(f"Received: {result}")
yield "done"
coro = old_coroutine()
print(next(coro)) # "ready" - 预激
print(coro.send("hello")) # Received: hello, "done"
# async/await 协程 (新式)
async def new_coroutine():
await asyncio.sleep(1)
return "done"
# yield vs await
# ┌─────────────────────────────────────────────────────────────────────────┐
# │ 特性 yield await │
# │ ──────────────────────────────────────────────────────────────────── │
# │ 类型 生成器函数 异步函数 │
# │ 返回值 生成器对象 协程对象 │
# │ 双向通信 支持 (yield/send) 不直接支持 │
# │ 使用场景 迭代器、旧式协程 异步 I/O │
# │ 事件循环 不需要 需要 │
# └─────────────────────────────────────────────────────────────────────────┘
# await 的本质
async def demo():
# await x 等价于:
# 1. 挂起当前协程
# 2. 等待 x 完成
# 3. 恢复执行,返回结果
result = await some_async_operation()
return result
考点 6: 信号处理机制
面试题:Python 如何处理信号?为什么需要自定义信号处理器?
参考答案:
python
import signal
import sys
# 常见信号
# ┌─────────────────────────────────────────────────────────────────────────┐
# │ 信号 编号 说明 默认行为 │
# │ ──────────────────────────────────────────────────────────────────── │
# │ SIGTERM 15 终止信号(可捕获) 终止进程 │
# │ SIGKILL 9 强制终止(不可捕获) 立即终止 │
# │ SIGINT 2 中断(Ctrl+C) 终止进程 │
# │ SIGHUP 1 挂起(终端关闭) 终止进程 │
# └─────────────────────────────────────────────────────────────────────────┘
# 默认行为的问题
# 收到 SIGTERM → 进程立即终止 → 不执行 finally 块 → 资源泄漏
# 自定义信号处理器
def sigterm_handler(signum, frame):
"""
signum: 信号编号
frame: 当前栈帧对象
"""
print(f"Received signal {signum}")
sys.exit(0) # 触发 SystemExit,执行 finally
signal.signal(signal.SIGTERM, sigterm_handler)
# 实际应用
def run_server():
def handler(signum, frame):
print("Shutting down gracefully...")
sys.exit(0)
signal.signal(signal.SIGTERM, handler)
signal.signal(signal.SIGINT, handler)
try:
# 启动服务
start_service()
# 等待
while True:
time.sleep(1)
except SystemExit:
print("Received exit signal")
finally:
# 清理资源
cleanup_resources()
print("Cleanup completed")
# 注意事项
# 1. 信号处理器应该尽量简单
# 2. 避免在处理器中进行 I/O 操作
# 3. 使用标志位或 sys.exit() 通知主循环
# 4. SIGKILL 无法被捕获
3.3 代码分析题
考点 7: 分析子进程启动代码
面试题:请分析以下代码的设计意图和潜在问题:
python
def run(address: str, logging_conf: Optional[Dict] = None):
def sigterm_handler(signum, frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
loop = asyncio.get_event_loop()
task = loop.create_task(_start_supervisor(address, logging_conf))
loop.run_until_complete(task)
参考答案:
python
# 设计意图分析
# 1. 信号处理器
def sigterm_handler(signum, frame):
sys.exit(0)
# 意图:实现优雅退出
# 原理:sys.exit(0) 触发 SystemExit 异常,执行 finally 块
# 好处:确保资源清理(GPU、文件、网络连接等)
# 2. 事件循环
loop = asyncio.get_event_loop()
# 意图:获取异步执行环境
# 原因:
# - spawn 模式下子进程不继承父进程的事件循环
# - 每个进程需要独立的事件循环
# - asyncio 架构需要事件循环支持
# 3. 异步任务
task = loop.create_task(_start_supervisor(...))
# 意图:将协程调度到事件循环
# 好处:
# - 任务立即进入队列开始调度
# - 可以添加多个任务实现并发
# - 可以取消任务
# 4. 阻塞等待
loop.run_until_complete(task)
# 意图:运行事件循环直到任务完成
# 行为:阻塞调用,直到 _start_supervisor 完成
# 潜在问题和改进建议
# 问题 1: 没有异常处理
# 改进:
def run_improved(address: str, logging_conf: Optional[Dict] = None):
def sigterm_handler(signum, frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
loop = asyncio.get_event_loop()
task = loop.create_task(_start_supervisor(address, logging_conf))
try:
loop.run_until_complete(task)
except asyncio.CancelledError:
print("Task was cancelled")
except Exception as e:
print(f"Error: {e}")
raise
finally:
loop.close()
# 问题 2: 没有处理 SIGINT (Ctrl+C)
# 改进:
def sigint_handler(signum, frame):
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGINT, sigint_handler)
# 问题 3: 事件循环可能已存在
# 改进:
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 问题 4: 资源清理
# 改进: 在 _start_supervisor 中添加清理逻辑
async def _start_supervisor(...):
pool = None
try:
pool = await create_actor_pool(...)
# ... 启动逻辑
await pool.join()
except asyncio.CancelledError:
if pool:
await pool.stop()
raise
考点 8: 并发模式选择
面试题:以下场景应该选择哪种并发模式?
参考答案:
python
# 场景 1: 批量图像处理 (CPU 密集型)
# 答案:多进程 + ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
def process_image(image_path):
# CPU 密集型操作
import cv2
img = cv2.imread(image_path)
# 处理...
return result
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(process_image, image_paths))
# 场景 2: 爬虫抓取多个网页 (I/O 密集型,高并发)
# 答案:协程 + aiohttp
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def crawl(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks)
# 场景 3: 简单的数据库查询 (I/O 密集型,少量并发)
# 答案:多线程 + ThreadPoolExecutor
from concurrent.futures import ThreadPoolExecutor
def query_database(sql):
# I/O 操作
return db.execute(sql)
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(query_database, sql) for sql in queries]
# 场景 4: 深度学习推理 (需要 GPU/CUDA)
# 答案:多进程 spawn 模式
import multiprocessing
multiprocessing.set_start_method("spawn")
def inference_worker(model_path):
import torch
model = torch.load(model_path)
# 推理...
p = multiprocessing.Process(target=inference_worker, args=(model_path,))
p.start()
p.join()
# 场景 5: Web 服务器处理请求
# 答案:协程 (FastAPI/Starlette)
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
await some_async_operation()
return {"message": "Hello"}
# 场景 6: 数据管道 (混合型)
# 答案:多进程 + 协程
def worker_process():
async def process():
await asyncio.gather(*[task() for _ in range(100)])
asyncio.run(process())
processes = [multiprocessing.Process(target=worker_process) for _ in range(4)]
3.4 实践应用题
考点 9: 实现优雅关闭
面试题:如何实现一个支持优雅关闭的服务?
参考答案:
python
import asyncio
import signal
import sys
from typing import Set
class GracefulServer:
"""支持优雅关闭的异步服务器"""
def __init__(self):
self._running = True
self._tasks: Set[asyncio.Task] = set()
self._shutdown_event = asyncio.Event()
async def handle_request(self, request_id: int):
"""处理请求"""
print(f"Processing request {request_id}")
try:
await asyncio.sleep(5) # 模拟处理
print(f"Request {request_id} completed")
except asyncio.CancelledError:
print(f"Request {request_id} cancelled, cleaning up...")
raise
async def run(self):
"""主循环"""
# 注册信号处理器
loop = asyncio.get_event_loop()
def shutdown_handler():
print("Shutdown signal received")
self._running = False
self._shutdown_event.set()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, shutdown_handler)
# 模拟接收请求
request_id = 0
while self._running:
request_id += 1
task = asyncio.create_task(self.handle_request(request_id))
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
await asyncio.sleep(1) # 模拟请求间隔
# 等待所有任务完成
print("Waiting for tasks to complete...")
await asyncio.gather(*self._tasks, return_exceptions=True)
print("All tasks completed")
async def shutdown(self, timeout: float = 30.0):
"""关闭服务"""
self._running = False
# 取消所有任务
for task in self._tasks:
task.cancel()
# 等待任务完成或超时
try:
await asyncio.wait_for(
asyncio.gather(*self._tasks, return_exceptions=True),
timeout=timeout
)
except asyncio.TimeoutError:
print("Shutdown timeout, forcing exit")
async def main():
server = GracefulServer()
try:
await server.run()
finally:
await server.shutdown()
if __name__ == "__main__":
asyncio.run(main())
考点 10: 进程池 + 协程混合
面试题:如何结合进程池和协程处理混合型任务?
参考答案:
python
import asyncio
from concurrent.futures import ProcessPoolExecutor
from typing import List, Any
import multiprocessing
def cpu_intensive_task(data: Any) -> Any:
"""CPU 密集型任务"""
# 在进程池中执行
result = 0
for i in range(1000000):
result += i
return result
async def io_intensive_task(url: str) -> Any:
"""I/O 密集型任务"""
# 在协程中执行
await asyncio.sleep(0.1)
return f"Result from {url}"
class HybridProcessor:
"""混合型任务处理器"""
def __init__(self, max_workers: int = None):
self._max_workers = max_workers or multiprocessing.cpu_count()
self._executor = None
async def __aenter__(self):
self._executor = ProcessPoolExecutor(max_workers=self._max_workers)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self._executor:
self._executor.shutdown(wait=True)
async def run_cpu_task(self, data: Any) -> Any:
"""在进程池中运行 CPU 任务"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
self._executor,
cpu_intensive_task,
data
)
async def run_io_task(self, url: str) -> Any:
"""运行 I/O 任务"""
return await io_intensive_task(url)
async def process_batch(
self,
cpu_data: List[Any],
io_urls: List[str]
) -> tuple:
"""批量处理混合任务"""
# 并发执行 CPU 和 I/O 任务
cpu_tasks = [self.run_cpu_task(d) for d in cpu_data]
io_tasks = [self.run_io_task(url) for url in io_urls]
cpu_results, io_results = await asyncio.gather(
asyncio.gather(*cpu_tasks),
asyncio.gather(*io_tasks)
)
return cpu_results, io_results
async def main():
async with HybridProcessor(max_workers=4) as processor:
cpu_data = list(range(10))
io_urls = [f"http://example.com/{i}" for i in range(20)]
cpu_results, io_results = await processor.process_batch(
cpu_data,
io_urls
)
print(f"CPU results: {len(cpu_results)}")
print(f"I/O results: {len(io_results)}")
if __name__ == "__main__":
multiprocessing.set_start_method("spawn")
asyncio.run(main())
3.5 面试要点总结
┌─────────────────────────────────────────────────────────────────────────────┐
│ 面试核心要点 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 并发模式选择 │
│ ├─ CPU 密集型 → 多进程 │
│ ├─ I/O 密集型(少量)→ 多线程 │
│ ├─ I/O 密集型(大量)→ 协程 │
│ └─ 需要 CUDA → 多进程 spawn 模式 │
│ │
│ 2. GIL 理解 │
│ ├─ 作用:保护解释器状态,简化内存管理 │
│ ├─ 影响:多线程无法真正并行执行 Python 代码 │
│ └─ 规避:多进程、C 扩展、协程 │
│ │
│ 3. 进程启动方式 │
│ ├─ fork:复制父进程,快但不安全 │
│ ├─ spawn:全新进程,安全但慢,支持 CUDA │
│ └─ forkserver:折中方案 │
│ │
│ 4. 协程核心概念 │
│ ├─ 事件循环:调度协程、处理 I/O │
│ ├─ async/await:协程语法 │
│ ├─ Task:协程的封装 │
│ └─ 并发 vs 并行:协程是并发,不是并行 │
│ │
│ 5. 信号处理 │
│ ├─ SIGTERM:可捕获,用于优雅退出 │
│ ├─ SIGKILL:不可捕获,强制终止 │
│ └─ 自定义处理器:确保资源清理 │
│ │
│ 6. 最佳实践 │
│ ├─ 资源清理:finally 块、上下文管理器 │
│ ├─ 超时控制:wait_for、信号量限制并发 │
│ ├─ 异常处理:gather(return_exceptions=True) │
│ └─ 优雅关闭:信号处理器 + 任务取消 + 清理 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘