一文读懂python并发&并行编程--以xinference框架应用为例

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)

进程池是一种"预先创建、复用资源、限制数量"的并发管理策略。它主要用于解决频繁创建和销毁进程带来的巨大性能开销,并能有效防止系统因进程过多而崩溃。

在计算机层面,进程池的工作流程如下:

  1. 初始化 (Initialization):

    主程序启动时,根据配置(通常是CPU核心数),预先创建好 N 个空闲的工作进程 (Worker Processes)。

  2. 提交任务 (Submit):

    当有新任务(如处理一个文件、计算一个数学题)到来时,主进程将任务放入一个任务队列 (Task Queue) 中。

  3. 调度与执行 (Dispatch & Execute):

    池中的空闲进程会从队列中"抢"任务来执行。

  4. 复用与回收 (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()

最佳实践要点

  1. 设置启动方式 :在主模块开头设置 spawn 模式
  • 避免副作用:spawn 模式会启动一个全新的 Python 解释器进程,只导入主模块来执行子进程的代码。这避免了 fork 模式(Unix/Linux 默认)可能带来的问题。fork 会复制父进程的整个内存空间,如果父进程中有一些未释放的资源或复杂的 C 扩展库状态,子进程可能会继承一个"不干净"的状态,导致难以预料的崩溃或错误。
  • 保证跨平台一致性:使用 spawn 可以确保你的程序在 Windows、macOS 和 Linux 上的行为基本一致,减少了因平台差异导致的 bug。
  1. 守护进程 :辅助进程设置 daemon=True
  • 守护进程(Daemon Process)是一个在后台运行的进程,它的生命周期依赖于主进程。
  • 防止程序无法正常退出:当一个 Python 程序结束时,它会等待所有非守护进程的子进程执行完毕。如果你的某个子进程因为 bug 进入了死循环,主进程就会一直卡住,无法正常退出。
  • 自动清理:将那些不重要的、辅助性的后台任务(如心跳检测、日志写入)设置为守护进程。当主进程结束时,这些守护进程会被立即强制终止,从而保证主程序能够顺利退出。
  • 注意:守护进程不应执行关键任务,因为它可能在任何时刻被强制中断,没有机会执行清理代码。
  1. 信号处理:子进程注册信号处理器实现优雅退出
  • 当在终端按下 Ctrl+C 或使用 kill 命令时,操作系统会向进程发送一个信号(如 SIGINT 或 SIGTERM)。信号处理就是让你的程序能够"捕获"这些信号,并做出相应的反应。
  • 通过注册一个信号处理器,你可以在进程退出前执行一些必要的清理操作,例如:保存进度、关闭网络连接、释放锁等。
  1. 资源清理 :使用 finally 确保清理
  • 无论程序是正常结束还是因为异常而中断,finally 代码块中的代码都一定会被执行。
  • 保证资源释放:在多进程环境中,进程可能会因为各种意外情况(如未捕获的异常、外部信号)而终止。将资源清理代码(如 file.close(), connection.close())放在 finally 块中,可以确保无论发生什么,这些宝贵的系统资源都能被正确释放,避免资源泄漏。
  1. 避免全局状态:进程间不共享全局变量
  • 每个进程都有自己独立的内存空间。这意味着在一个进程中修改一个全局变量,不会影响到其他进程中的同名变量。
  • 如果确实需要在进程间共享数据,必须使用专门的进程间通信(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 创建和管理大量线程存在两个主要问题:

  1. 性能开销大:创建和销毁线程是昂贵的操作,会消耗系统资源。
  2. 资源失控:如果不加限制地创建线程,可能会耗尽系统内存或 CPU 资源,导致程序崩溃。

线程池通过复用一组预先创建好的线程来解决这些问题。当你需要执行任务时,只需将任务提交给线程池,池会分配一个空闲线程来处理。任务完成后,线程不会被销毁,而是回到池中等待下一个任务。

1.3.5 使用场景
场景 是否适用 原因
网络 I/O 操作 等待期间释放 GIL
文件 I/O 操作 等待期间释放 GIL
GUI 应用程序 保持界面响应
CPU 密集型计算 GIL 限制,无法并行
需要真正并行 受 GIL 限制
1.3.6 最佳实践

最佳实践要点

  1. 避免 CPU 密集型任务:多线程不适合 CPU 密集型

  2. 使用线程池:避免频繁创建销毁线程

  3. 设置超时:网络请求设置超时,避免阻塞

  4. 异常处理:每个任务独立处理异常

    子线程里的报错不会自动"传染"给主线程,你必须显式地去捕获或检查它,否则程序可能会在"看似正常"的情况下丢失数据或静默失败。

    在 Python 的 threading 模块中,如果子线程抛出了异常且没有在内部捕获,默认行为是打印错误堆栈到 stderr,然后该线程静默退出。主线程通常感知不到子线程已经挂了,它会继续运行,仿佛什么都没发生。

  5. 限制线程数:根据 I/O 等待时间设置合理线程数

    线程池的大小不是拍脑袋决定的(比如随便写个 100),而是要根据"CPU 计算时间"和"I/O 等待时间"的比例来科学计算,以达到资源利用率的最大化。

    为什么要根据 I/O 时间设置?

    1. 如果线程数太少:当所有线程都在等待 I/O(比如都在等数据库返回)时,CPU 就会空闲下来,浪费了计算资源。
    2. 如果线程数太多:虽然 CPU 不空闲了,但过多的线程会导致频繁的上下文切换(CPU 在线程间切换需要消耗时间),反而降低性能,甚至导致内存溢出。

1.4 协程模式 (Coroutine/asyncio)

1.4.1 核心概念

一种用户态的、轻量级的、协作式的并发编程模型。允许在单个线程内高效地管理多个执行流,通过主动"挂起"和"恢复"来避免阻塞,从而在处理大量 I/O 密集型任务时表现出极高的性能。

由用户态的运行时(如 Python 的 asyncio 库)管理。创建和切换只涉及用户态的少量指令,开销极低。每个协程的栈可以小到 KB 甚至几十字节。因此,单个线程可以轻松承载数十万甚至上百万个协程。

你可以将协程模式想象成一个高效的单线程厨师:

他同时处理多个订单(协程)。当一个订单需要等待烤箱(I/O 操作)时,他不会傻站着,而是立刻挂起这个订单的处理,转而去准备另一个订单的食材(恢复并执行另一个协程)。整个过程由他自己(事件循环)高效调度,无需其他厨师(线程)帮忙,就能实现惊人的并发处理能力。

因此,协程模式是解决高并发 I/O 密集型任务(如 Web 服务器、网络爬虫、数据库驱动)的理想选择。

核心机制:

  1. 挂起与恢复
    这是协程实现并发的核心机制。
  • 挂起 (Suspend):协程在执行到 await 关键字时,会暂停自己的执行,保存当前的执行上下文(如局部变量、指令指针),并将控制权交还给事件循环。
  • 恢复 (Resume):当 await 的异步操作(如网络响应到达)完成后,事件循环会重新调度该协程,从它上次挂起的地方继续执行。
    这个过程对开发者来说是透明的,代码看起来是同步的、线性的,但底层却是非阻塞的异步执行。
  1. 事件循环
    事件循环是协程的"大脑"和调度中心。它在一个单线程中持续运行,主要负责:
  • 维护任务队列:管理所有待执行的协程。
  • 监听 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())

最佳实践要点

  1. 使用信号量限制并发:避免资源耗尽
  2. 设置超时 :使用 wait_for 防止无限等待
  3. 异常处理gather(return_exceptions=True) 捕获异常
  4. 优雅关闭 :处理 CancelledError,清理资源
  5. 使用 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)

详解

  1. 信号处理器定义

    python 复制代码
    def sigterm_handler(signum, frame):
        sys.exit(0)
    • signum:信号编号,SIGTERM = 15
    • frame:当前的栈帧对象,包含调用栈信息
    • sys.exit(0):触发 Python 的退出机制
  2. 注册信号处理器

    python 复制代码
    signal.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

执行步骤为:

  1. ✅ 打印 "Running..."
  2. ⏸️ 程序进入 time.sleep(1) 循环
  3. ⌨️ 用户按下 Ctrl + C
  4. 🔔 操作系统发送 SIGINT 信号
  5. ❌ 代码没有为 SIGINT 注册处理函数
  6. 🐍 Python 将 SIGINT 转换为 KeyboardInterrupt 异常
  7. ❌ except SystemExit 无法捕获 KeyboardInterrupt
  8. ✅ finally 块总是执行,打印 "Finally block executed"
  9. 🚫 KeyboardInterrupt 未被捕获,程序终止

在另一个终端执行 kill # 发送 SIGTERM 时

sh 复制代码
# 输出:
Running...
Signal 15 received
SystemExit caught, cleaning up...
Finally block executed
2.3.2 步骤二:获取事件循环
python 复制代码
loop = asyncio.get_event_loop()

详解

  1. get_event_loop() 的行为

    • get_event_loop() 在主线程首次调用时会自动创建循环
    • 每个线程只能有一个事件循环, 如果当前线程已有事件循环,返回它
    • 如果没有,创建并返回新的事件循环
    • 主线程默认没有事件循环,需要手动创建
  2. 事件循环的生命周期

    复制代码
    ┌─────────────────────────────────────────────────────────────────┐
    │                   事件循环生命周期                               │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                 │
    │   loop = asyncio.get_event_loop()                              │
    │      │                                                          │
    │      ▼                                                          │
    │   创建/获取事件循环                                              │
    │      │                                                          │
    │      ▼                                                          │
    │   loop.run_until_complete(task)                                │
    │      │                                                          │
    │      ▼                                                          │
    │   事件循环开始运行                                               │
    │      │                                                          │
    │      ├─ 调度任务                                                │
    │      ├─ 处理 I/O 事件                                           │
    │      ├─ 执行回调                                                │
    │      │                                                          │
    │      ▼                                                          │
    │   任务完成                                                      │
    │      │                                                          │
    │      ▼                                                          │
    │   事件循环停止                                                  │
    │      │                                                          │
    │      ▼                                                          │
    │   返回任务结果                                                  │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘

为什么在子进程中创建事件循环?

  1. 进程隔离:每个进程有独立的内存空间,需要独立的事件循环
  2. spawn 模式:子进程启动全新的 Python 解释器,不继承父进程的事件循环
  3. 异步架构:Xinference 使用 asyncio 构建,需要事件循环支持
2.3.3 步骤三:创建异步任务
python 复制代码
task = loop.create_task(
    _start_supervisor(address=address, logging_conf=logging_conf)
)

详解

  1. create_task() 的作用

    python 复制代码
    task = loop.create_task(coro)
    • 将协程对象封装为 Task 对象
    • 将任务添加到事件循环的待执行队列
    • 任务立即开始调度(不需要等待 run_until_complete)
  2. 协程 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 立即进入事件循环的任务队列
  3. 多任务并发

    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))
  4. 任务的状态管理

    python 复制代码
    import 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)

详解

  1. run_until_complete() 的作用

    • 启动事件循环
    • 运行直到指定的任务完成
    • 返回任务的结果
    • 自动停止事件循环
  2. 执行流程

    复制代码
    ┌─────────────────────────────────────────────────────────────────┐
    │              run_until_complete() 执行流程                      │
    ├─────────────────────────────────────────────────────────────────┤
    │                                                                 │
    │   loop.run_until_complete(task)                                │
    │      │                                                          │
    │      ▼                                                          │
    │   启动事件循环                                                   │
    │      │                                                          │
    │      ▼                                                          │
    │   ┌─────────────────────────────────────────────────────────┐   │
    │   │                    事件循环迭代                          │   │
    │   │                                                         │   │
    │   │   while not task.done():                                │   │
    │   │       1. 检查就绪的任务                                  │   │
    │   │       2. 执行任务到 await                                │   │
    │   │       3. 检查 I/O 事件 (selector)                        │   │
    │   │       4. 处理定时器                                      │   │
    │   │       5. 唤醒等待的协程                                  │   │
    │   │                                                         │   │
    │   └─────────────────────────────────────────────────────────┘   │
    │      │                                                          │
    │      ▼                                                          │
    │   task 完成                                                     │
    │      │                                                          │
    │      ▼                                                          │
    │   停止事件循环                                                   │
    │      │                                                          │
    │      ▼                                                          │
    │   返回 task.result()                                            │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
  3. 阻塞特性

    python 复制代码
    # run_until_complete 是阻塞调用
    print("Before run_until_complete")
    loop.run_until_complete(task)  # 阻塞在这里
    print("After run_until_complete")  # task 完成后执行
  4. 与 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)                            │
│      └─ 优雅关闭:信号处理器 + 任务取消 + 清理                               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

4. 参考资料

相关推荐
dollmarker2 小时前
vulnhub靶场之hacksudo: 2 (HackDudo)靶机-NFS提权
c语言·网络·网络安全·php
liulilittle2 小时前
递归复制搜索所有的lua文件到指定目录
java·开发语言·lua·cmd
测试19982 小时前
Selenium自动化测试框架的搭建
自动化测试·软件测试·python·selenium·测试工具·职场和发展·测试用例
Gofarlic_oms12 小时前
Allegro高级功能模块许可证管理注意事项
运维·服务器·开发语言·matlab·负载均衡
IMPYLH2 小时前
Linux 的 sum 命令
linux·运维·服务器·chrome·python·bash
启山智软2 小时前
前沿主流技术栈商城系统(Java JDK21 + Vue3 + Uniapp)
java·开发语言·uni-app
qq_392690662 小时前
如何处理MongoDB分片集群的连接池耗尽危机_客户端连接与mongos到shard的连接乘数效应
jvm·数据库·python
qq_372154232 小时前
Python异步爬虫如何应对封IP_结合asyncio与代理池实现轮询请求
jvm·数据库·python
abc123456sdggfd2 小时前
php怎么处理跨域请求_php如何设置header解决跨域问题详解
jvm·数据库·python