FastAPI学习笔记(2):前期准备2_并行与并发简介

132 阅读6分钟

一、什么是并行与并发(进程、线程和协程)

1.1、定义:

并行(Parallelism): 多个任务在同一时刻真正同时执行

并发(Concurrency): 多个任务在同一时间段内交错推进,系统在它们之间快速切换,使得宏观上看起来“同时进行”

1.2、举例说明:

想象你开餐厅,需要同时做3道菜:红烧肉、蒸鱼和炖汤

模式1:进程——开3家餐厅(并行)

  • 在3个地点开了3家餐厅(3个进程
  • 每家餐厅都有自己的厨房、厨师和食材(独立内存空间
  • 三家餐厅可以同时开工,互不影响
  • 优点:一家店出问题(比如进程崩溃),不会影响其他餐厅运营(隔离性好)
  • 缺点:成本高,资源浪费

模式2:线程——一个厨房,多个厨师(并发,受限并行)

  • 只有一家餐厅,但是有3个厨师(3个线程

  • 但是一个厨房,3个厨师共用(共享内存空间

  • 关键问题:

    • 线程在CPU 上理论上可以实现并行(就像多个厨师同时在厨房干活),但在 CPython 解释器中,由于存在一个叫 GIL(全局解释器锁)的“总管”,它规定:同一时刻,只能有一个线程执行 Python 代码(即只有一个厨师能拿着菜谱在厨房操作)。
    • 因此,对于 CPU 密集型任务(比如切菜、炒菜等需要持续计算的操作),多个线程无法真正并行,只能轮流使用厨房,实际仍是串行执行。
    • 但是,当某个线程执行 I/O 操作(比如把肉拿出来化冻、等水烧开、等待网络响应)时,它会主动释放 GIL 并进入等待状态。这时,“经理”(操作系统或 Python 解释器)就会让另一个线程进入厨房继续工作。
    • 通过这种方式,多个线程可以在 I/O 等待期间交替执行,从而实现并发——虽然不是真正的并行,但在 I/O 密集型场景下能显著提升效率。
  • 什么情况下,换厨师进入厨房进行工作(切换任务):

    1)时间片用完

    • 比如经理分给每个厨师0.5个小时用厨房,到时间就换厨师

    2)线程主动阻塞

    • 比如菜化冻,等水烧开等需要等待的任务时,换厨师进行工作
  • 缺点: 一个线程崩溃,通常会导致整个进程崩溃

模式3:协程——一个厨师,合理规划任务(并发)

  • 只有一个餐厅(进程),一个厨师(线程)

  • 他会合理规划任务,能在任务间快速切换

  • 示例:

    • 从冰箱中取出菜化冻(I/O等待),此时不等化冻完毕,先去切葱
    • 当化冻完成的通知到达(I/O完成),自动切回该任务,洗菜
    • 烧油
    • 将葱和菜放入锅中进行炒菜
  • 关键点:

    • 厨师(线程)在同一时间只做一件事
    • 但通过主动让出(await)和事件通知,实现并发
    • 因为整个厨房只有他一个人干活,不需要和其他厨师协调资源,所以不存在 GIL 争用问题(GIL 依然存在,但不会成为瓶颈)
    • 任务切换由程序自己控制,无需操作系统介入,因此上下文切换开销极小,非常适合高并发的 I/O 密集型场景

1.3、线程和协程实现并发的区别:

根据上述内容,线程和协程都可以实现并发,那么使用线程和协程实现并发具体有什么区别呢?

image.png

二、异步函数与事件循环

2.1、异步函数

(1)定义:

异步函数(async function) 是一种特殊的函数,它使用 "async def" 定义,调用时不会立即执行函数体,而是返回一个协程对象(coroutine)

它允许程序在等待耗时操作(如网络请求、文件读写)时,主动让出控制权,去执行其他任务,从而实现高并发

(2)基本语法与注意事项

async def my_async_function():
    # 异步操作,例如 await 网络请求
    result = await some_async_operation()
    return result

注意:

  • "await"后必须是一个可等待对象,"await"告诉python,需要等待"await"后的代码完成并返回结果,在此期间,会让出控制权,python可以先执行其它代码,当“await”后的代码执行完毕,返回结果后,再切换回来,执行后续代码
  • 要使"await"工作,它必须支持这种异步机制的函数内,及"await"必须位于使用"async def"声明的函数(异步函数)中,而执行异步函数,又必须在前添加"await",否则只会返回一个协程对象,而不是函数执行的结果。
  • 这就带来了一个问题,"await"必须位于异步函数中,而执行异步函数又需要添加"await",变成了先有鸡还是先有蛋的问题,带来疑问,该如何执行最顶层的异步函数? 具体参见下一节"事件循环"。

2.2、事件循环

(1)定义:

一个持续运行的循环机制,是 asyncio 的核心调度器,负责:

  • 接收并管理待执行的协程(coroutines);
  • 当协程遇到"await"时,将其挂起,并调度其他就绪的协程;
  • 当被等待的异步操作(如 I/O)完成后,恢复对应协程的执行

(2)事件循环启动异步函数:

在包含异步函数的脚步中,对于最顶层的异步函数/主函数,需要启动一个事件循环,通过事件循环运行函数,例如:

"asyncio.run(main())"实现了以下效果:

  • 创建一个事件循环。
  • 把"main()" 这个协程(coroutine)放入循环中运行。
  • 当"main()" 结束时,关闭事件循环。

常见错误: 直接调用异步函数

如果你这样写:

main()

你会得到警告或什么也不发生,因为"main()"返回的是一个 协程对象(coroutine object),而不是执行它。

(3)不同场景启动最顶层异步函数的区别

image.png

重点:

1)Jupyter 的单元格执行器内置了事件循环支持,可以自动处理顶层的"await",不需要手动启动事件循环

当你在jupyter中执行以下代码:

result = await some_async_function()
print(result)

jupyter的内核(Kernel)实际上在执行时做了类似这样的事:

# 伪代码:Jupyter 内部的执行逻辑
async def hidden_main():
    result = await some_async_function()
    print(result)

# Jupyter 自动为你运行
asyncio.run(hidden_main())

2)在FastAPI等Web框架中,服务器启动事件循环

from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.get("/slow")
async def slow_endpoint():
    await asyncio.sleep(5)
    return {"message": "Done!"}
  • 不需要写"asyncio.run()"
  • 当你用"uvicorn main:app --reload"启动时:
  • Uvicorn 会创建一个或多个工作进程。
  • 在每个工作进程中,它会启动一个事件循环