计算机系统

1. window & linux 多进程机制

1. 进程创建机制:Fork vs. Spawn

特性 Linux / macOS (fork) Windows (spawn)
创建速度 极快。利用“写时复制”技术。 较慢。需启动全新的 Python 解释器。
初始状态 父进程的镜像副本(包含内存、变量)。 空白状态。需重新加载模块、初始化环境。
内存占用 。初始阶段与父进程共享物理内存。 。每个子进程都有独立的内存消耗。
数据传递 直接访问父进程资源(逻辑隔离,物理共享)。 必须通过 Pickle 序列化进行 IPC 通信。

2. 核心技术:写时复制 (Copy-on-Write, COW)

fork 所谓的“内存共享”本质上是一种延迟拷贝的策略:

  • 初始阶段(假共享):父子进程的虚拟地址指向同一块只读物理内存,创建过程几乎不耗时。

  • 触发阶段(页错误):当任意进程尝试修改内存时,CPU 触发中断。

  • 执行阶段(真隔离):内核仅为被修改的那一页数据分配新物理空间并复制内容。

  • 优势:极大减少了不必要的内存拷贝,提升了系统响应速度。


3. 性能差距的实际影响

  • 启动开销:在 Windows 上,频繁创建/销毁进程(如处理大量小任务)会导致严重的性能抖动。在 Linux 上,进程创建的廉价性使其更接近线程的体验。

  • 序列化限制spawn 要求所有传递的参数必须可序列化(Picklable)。如果对象涉及复杂的 C 扩展或未绑定的方法,在 Windows 上会报错,而在 Linux 上则能直接运行。


4. 跨平台最佳实践(避坑指南)

🛡️ if __name__ == '__main__': 保护块

  • 原因:Windows 的 spawn 会重新导入主模块。如果没有此保护块,子进程导入时会再次触发创建子进程的代码,导致无限递归循环

  • 结论:无论在什么平台,始终将启动代码放入此块中是标准规范。

🚀 任务量级匹配

  • 短平快任务:在 Windows 上避免使用多进程,启动开销可能远超计算耗时。

  • 长耗时任务:多进程在两平台均能显著提升性能。


5. 总结比喻

  • Linux (fork)细胞分裂:瞬间一分为二,初始状态一模一样,只有后续生长(修改数据)时才各过各的。

  • Windows (spawn)克隆工厂:重新建一个工厂(解释器),再把图纸(数据)打包快递(序列化)过去。

2. 进程,线程,事件循环,协程

进程(Process)

  • 作用:操作系统级“隔离的执行单元”。提供独立的内存空间、文件描述符表、资源隔离与崩溃隔离。
  • 执行范围:一个进程内包含自己的地址空间与资源;进程之间默认不共享内存(需要 IPC/共享内存等)。
  • 执行数量:一个程序可起 多个进程;常见 Web 部署用 “N 个 worker 进程” 来利用多核与隔离故障。
  • 执行流程(概念)
    • OS 创建进程 → 装载程序 → 分配资源 → 进程内启动一个或多个线程运行代码 → 进程结束释放资源。

线程(Thread)

  • 作用:进程内的“并发执行单元”。线程共享进程内存与大部分资源,用于并发执行或隐藏 I/O 等待。
  • 执行范围:同一进程的线程共享堆/全局变量/打开的文件等;每个线程有自己的栈与寄存器上下文。
  • 执行数量:一个进程可有 多个线程
    • Python 里线程能并发做 I/O;但 CPU 密集并行受 GIL 影响(同一进程内同一时刻通常只有一个线程在执行 Python 字节码)。
  • 执行流程(概念)
    • 线程创建 → OS 调度线程时间片 → 线程运行/阻塞/唤醒 → 上下文切换 → 线程退出。

事件循环(Event Loop)

  • 作用:在单个线程中,用“调度 + 轮询 I/O 就绪事件”的方式,驱动大量并发的异步任务。
  • 执行范围:通常 一个事件循环绑定一个线程(最常见)。该线程负责:
    • 运行协程的“继续执行片段”
    • 等待/处理 I/O 就绪(socket、定时器等)
    • 调度回调/任务切换
  • 执行数量:常见是:
    • 每个进程 1 个事件循环(Web 服务器 worker 模型里很常见)
    • 也可以一个进程多个事件循环(分别在不同线程里),但复杂度更高。
  • 执行流程(概念)
    • 循环开始 → 取出“可运行任务”执行一小段 → 遇到 await/I/O 等待则挂起任务 → 轮询 I/O 就绪与定时器 → 就绪后恢复对应任务 → 反复。

协程(Coroutine / async task)

  • 作用:是一种可中断且可恢复的特殊函数,它不像普通函数那样“一走到底”,而是在执行到特定标记(如 await)时,将当前状态(变量、执行位置)挂起,并将控制权交还给事件循环(Event Loop),这种挂起是非阻塞的,保存的上下文让它能在稍后被事件循环重新唤醒并从断点继续。
  • 执行范围:协程不由 OS 直接调度,而是由事件循环所在某个线程里调度执行。协程的代码始终运行在“承载它的事件循环线程”上(除非显式丢到线程池/进程池)。
  • 执行数量:通常可以是 成千上万(远多于线程)。具体受内存、连接数、任务结构影响。
  • 执行流程(概念)
    • 创建协程对象 → 被事件循环包装成 Task → 运行到第一个 await 前的同步段 → await 时挂起并注册等待条件(I/O/定时器/另一个任务)→ 条件满足后继续执行 → 结束返回结果/异常。

时间循环与协程注意点:

  • Event Loop 的最小单位:事件循环不直接调度“协程对象”,它调度的是 Task(任务)

  • await coroutine(伪异步/同步等待): 如果你直接 await func(),当前协程会停下来原地等待 func 执行完。虽然它在事件循环里,但并没有产生“并发”效果,依然是线性顺序。

  • await task(真正的并发): 当你使用 asyncio.create_task(coro) 时,协程被包装成 Task 并立即注册到事件循环的就绪队列中。此时,Task 已经开始独立排队执行。当你随后 await task 时,你只是在检查它的运行结果,而在此之前,它可能已经利用你处理其他逻辑的时间运行了一半。

特性 进程/线程 (Process/Thread) 协程 (Coroutine)
调度权 操作系统 (OS) 强制掌控。 程序代码 (User) 自主掌控。
切换方式 抢占式 (Preemptive):OS 定时器强制中断当前线程(如 GIL 的 5ms 切换)。 协作式 (Cooperative):通过 await 明确表示“我现在没空,控制权还你”。
上下文切换开销 :涉及内核态切换、寄存器与栈的大规模保存。 极低:仅是简单的函数跳转和对象状态保存。
并发模型 适合 CPU 密集型(多进程)。 适合 I/O 密集型(单线程高并发)。

把四者放在一起看

  • 层级关系
    进程(隔离与多核)
    └── 线程(进程内并发;其中一个通常跑事件循环)
          └── 事件循环(调度中心,通常 1 个线程 1 个 loop)
              └── 协程/Task(海量并发的请求处理单元)

  • 关键结论

    • 协程并发的前提是:事件循环线程不能被阻塞
    • 所以同步阻塞 I/O 或 CPU 重活要么用异步驱动(让 await 生效),要么丢到线程池/进程池/任务队列,避免把事件循环“卡死”。

注意:

1. 事件循环是绑定到线程上的。 “每个进程 1 个事件循环”是工程实践里的常见部署形态,不是说事件循环“绑定到进程”。

更精确的表述应该是: - 每个进程里通常只有 1 个“事件循环线程”
- 因为事件循环需要跑在某个线程上,所以就表现为:每个 worker 进程内部有一个线程在跑一个事件循环

也就是:

1 进程 →(通常)1 线程跑 event loop → 这个 loop 调度该进程内的所有协程


2. 为什么会有人说“每个进程一个事件循环”?

在 Web 服务器常见模型里(例如 uvicorn/gunicorn worker):

  • 你会起多个 worker 进程(为了多核利用、隔离、稳定性)
  • 每个 worker 进程内部,为了简单和高性能,通常只用 一个主线程一个 event loop
  • 所以口头上就简化成“每个进程一个 event loop”

3. 能不能“一个进程多个事件循环”?

可以,但要满足绑定关系:

  • 一个线程最多跑一个 event loop
  • 所以 一个进程多个 event loop 意味着:这个进程里必须有 多个线程,每个线程各跑一个 loop

这种做法在 Python Web 服务里不常见,因为: - 增加复杂度(跨线程共享状态、连接池、上下文) - 很多库的使用方式默认“单 loop 单线程”更简单可靠 - 更常见的扩展方式是:多进程(worker 数)而不是“多 loop 多线程