【Python】深入理解Python Web框架:从Flask到FastAPI的并发之路

深入理解Python Web框架:从Flask到FastAPI的并发之路

前言

这篇是 Python 基础合集的学习笔记,这次整理的是 Python Web 框架的并发模型。

说实话,刚开始学 Python Web 开发的时候,我也是一头雾水。Flask 是同步的,FastAPI 是异步的------这话听了无数遍,但到底啥意思?GIL 不是说 Python 多线程是假的吗?那 Flask 的多线程又是怎么回事?async 函数里为啥必须用异步库?

这些问题困扰了我挺长一段时间。后来花了不少功夫,总算把这些概念串起来了。今天就把我的理解分享出来,希望能帮你少走点弯路。

🏠个人主页:山沐与山


文章目录


一、从一个餐厅的故事说起

在深入技术细节之前,先用一个生活中的例子来理解同步和异步。别急着跳过,这个例子后面会反复用到。

1.1 同步模式:一个"死脑筋"的服务员

想象一家餐厅,服务员小张的工作方式是这样的:

复制代码
客人A点餐 → 小张去厨房等菜做好 → 端菜给A → 才能服务下一桌

时间线:
客人A: |点餐|----等待20分钟----|上菜|
客人B:                              |点餐|----等待20分钟----|上菜|
客人C:                                                          |点餐|...

三桌客人总耗时:60分钟

小张在厨房干等着,什么都不干。你想想,这效率得多低?但他就是这么死脑筋------一件事没做完,绝不开始下一件。这就是同步阻塞

1.2 异步模式:一个"会来事"的服务员

现在小张开窍了:

复制代码
客人A点餐 → 小张把单子给厨房 → 不等!去服务客人B
客人B点餐 → 小张把单子给厨房 → 不等!去服务客人C
客人C点餐 → 小张把单子给厨房
厨房喊"A的菜好了" → 小张端菜给A
厨房喊"B的菜好了" → 小张端菜给B
...

时间线:
客人A: |点餐|----等待----|上菜|
客人B:      |点餐|----等待----|上菜|
客人C:           |点餐|----等待----|上菜|

三桌客人总耗时:约25分钟

看到没?小张在等待的时候去做别的事了,效率一下子就上来了。这就是异步非阻塞

1.3 映射到Web服务器

把这个餐厅的例子对应到 Web 服务器:

餐厅 Web服务器
服务员 线程/协程
客人点餐 收到HTTP请求
厨房做菜 数据库查询/调用外部API
等待上菜 等待I/O响应
服务完成 返回HTTP响应

Web 应用大部分时间都在干嘛?等!等数据库返回,等 Redis 响应,等第三方 API 回复。如果你的服务员(线程)傻等着,那资源就白白浪费了。


二、GIL------理解Python并发的钥匙

要理解 Python 的并发,必须先搞懂一个东西:GIL(全局解释器锁)。这玩意儿是很多 Python 并发问题的根源。

2.1 GIL是什么

复制代码
GIL = Global Interpreter Lock = 全局解释器锁

规则很简单:同一时刻,只有一个线程能执行 Python 字节码

就这么一句话,但理解它需要点背景知识。

2.2 为什么Python要设计这个东西

这要从 1991 年说起。那时候:

  • CPU 都是单核的,多核处理器要到 2005 年才开始普及
  • Python 使用引用计数来管理内存
python 复制代码
# Python 的内存管理:引用计数
a = []      # 创建列表,引用计数 = 1
b = a       # b也指向这个列表,引用计数 = 2
del b       # 删除b,引用计数 = 1
del a       # 删除a,引用计数 = 0 → 内存被释放

如果没有 GIL,两个线程同时修改引用计数会出问题:

复制代码
线程1: 读取引用计数(1) → 准备+1
                                    线程2: 读取引用计数(1) → 准备+1
线程1: 写入引用计数(2)
                                    线程2: 写入引用计数(2)

结果:引用计数 = 2,但实际应该是 3!
内存管理直接乱套

所以 Python 加了 GIL:同一时刻只有一个线程能跑 Python 代码,引用计数就不会乱。简单粗暴,但有效。

2.3 关键秘密:I/O时会释放GIL

这是很多人不知道的:当线程进行 I/O 操作时,会释放 GIL

python 复制代码
import requests

def fetch_data():
    response = requests.get("http://api.com")  # 发起网络请求
    # 这里!当请求发出去后,线程在等待网络响应
    # 此时 GIL 会被释放,其他线程可以执行
    return response

让我画个图:

复制代码
时间线 ────────────────────────────────────────────────────>

线程1: |执行Python代码|    |.....等待网络.....|    |执行Python代码|
           ↓ 持有GIL        ↓ 释放GIL                ↓ 持有GIL

线程2:    (等待GIL)    |执行代码|   |..等待DB..|       (等待GIL)
                          ↓           ↓ 释放GIL
                        持有GIL

线程3:    (等待GIL)    (等待GIL)   |执行代码|   |..等待文件..|
                                      ↓
                                    持有GIL

关键观察:
- "执行Python代码" → 永远只有1个线程(GIL限制)
- "等待I/O" → 可以多个线程同时等(不需要GIL)

2.4 一句话总结GIL

复制代码
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   Python多线程的真相:                                       │
│                                                             │
│   线程是真的 → 操作系统级别的真线程                          │
│   GIL是真的 → 同一时刻只有1个线程能执行Python代码             │
│   I/O等待是真并发 → 多个线程可以同时等待I/O                  │
│                                                             │
│   所以:"执行代码"是假并行,"等待I/O"是真并发                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘

这一点理解了,后面的内容就好懂多了。


三、Flask的工作方式------多线程模型

理解了 GIL,我们来看 Flask 是怎么处理并发的。

3.1 Flask的基本模型

python 复制代码
from flask import Flask
app = Flask(__name__)

@app.route('/user/<int:id>')
def get_user(id):
    user = db.query(f"SELECT * FROM users WHERE id = {id}")  # 同步查询
    return {"user": user}

Flask 使用 WSGI 协议 (Web Server Gateway Interface),这是一个同步协议 。2003 年设计的时候,Python 还没有 async/await 这些东西。

3.2 Flask处理并发的方式

Flask 自己不做并发,它把这事儿交给 WSGI 服务器来处理:

复制代码
                    Flask 并发模型

        ┌─────────────────────────────────┐
        │         WSGI 服务器              │
        │    (gunicorn / uwsgi)           │
        └───────────────┬─────────────────┘
                        │
        ┌───────────────┼───────────────┐
        ▼               ▼               ▼
   ┌─────────┐    ┌─────────┐    ┌─────────┐
   │ 线程1   │    │ 线程2   │    │ 线程3   │
   │         │    │         │    │         │
   │ 处理请求A│    │ 处理请求B│    │ 处理请求C│
   └─────────┘    └─────────┘    └─────────┘

每个请求分配一个线程,线程内部同步执行

你用 gunicorn -w 4 --threads 4 启动 Flask 应用,就是 4 个进程,每个进程 4 个线程,一共 16 个并发处理单元。

3.3 多线程的实际执行过程

结合前面讲的 GIL

复制代码
请求A → 线程1: |执行代码|----等待数据库----|执行代码|返回|
请求B → 线程2:          |执行代码|---等待---|执行代码|返回|
请求C → 线程3:                   |执行代码|---等待---|返回|

由于GIL:
- "执行代码"部分:线程轮流执行(假并行)
- "等待数据库"部分:真的可以同时等(真并发)

所以 Flask 的多线程不是完全没用------在等待 I/O 的时候,多个线程确实是"同时"在等的。

3.4 Flask多线程的代价

但这种方式有问题:

问题 说明
内存开销大 每个线程约 8MB 内存(线程栈),1000 并发 = 8GB 内存
切换开销 线程切换是操作系统级别的,有上下文切换成本
线程本身占资源 线程等待时虽然释放了 GIL,但线程本身还占着内存

你可能会想:Flask 这样用多线程处理 I/O 等待,和协程的效果差不多啊?

没错!某种意义上,Flask 是在用"重量级工具"(线程)做"轻量级的事"(等 I/O)。这就引出了 FastAPI 的方案。


四、FastAPI的工作方式------协程与事件循环

4.1 FastAPI的基本模型

python 复制代码
from fastapi import FastAPI
app = FastAPI()

@app.get('/user/{id}')
async def get_user(id: int):
    user = await db.fetch_one(f"SELECT * FROM users WHERE id = {id}")
    return {"user": user}

FastAPI 使用 ASGI 协议(Asynchronous Server Gateway Interface),专门为异步设计。

4.2 事件循环是什么

事件循环Event Loop)是 asyncio 的核心,可以理解为一个"超级调度员",在单线程里管理所有任务:

python 复制代码
# 事件循环的本质(伪代码)
def 事件循环():
    任务队列 = []

    while True:
        for 任务 in 任务队列:
            if 任务.准备好了():
                执行任务一小步()

                if 任务.遇到了await():
                    # 任务说"我要等东西,先跳过我"
                    把任务标记为等待中()

                if 任务.完成了():
                    从队列移除(任务)

        # 检查等待中的任务有没有等到想要的东西
        检查IO完成情况()

4.3 用一个真实例子理解事件循环

python 复制代码
import asyncio

async def 任务A():
    print("[A] 开始")
    await asyncio.sleep(2)  # A说:我要等2秒,先忙别的去
    print("[A] 结束")

async def 任务B():
    print("[B] 开始")
    await asyncio.sleep(1)  # B说:我要等1秒,先忙别的去
    print("[B] 结束")

async def main():
    await asyncio.gather(任务A(), 任务B())

asyncio.run(main())

事件循环的执行过程

复制代码
[0.0秒] 执行任务A,打印"[A] 开始"
[0.0秒] 任务A遇到 await sleep(2),暂停A,设置2秒后唤醒
[0.0秒] 切换到任务B,打印"[B] 开始"
[0.0秒] 任务B遇到 await sleep(1),暂停B,设置1秒后唤醒
[0.0秒] 没有可执行的任务了,事件循环等待...

[1.0秒] 任务B的sleep到期,唤醒B
[1.0秒] 任务B打印"[B] 结束",任务B完成

[2.0秒] 任务A的sleep到期,唤醒A
[2.0秒] 任务A打印"[A] 结束",任务A完成

输出顺序:

复制代码
[A] 开始
[B] 开始
[B] 结束  ← 1秒后
[A] 结束  ← 2秒后

总耗时:2秒(不是3秒!两个任务"同时"在等)

看到没?关键就在 await 这个关键字------它告诉事件循环"我要等东西,你先忙别的去"。

4.4 FastAPI架构图

复制代码
                        FastAPI 请求处理架构

                    ┌─────────────────────────┐
                    │      客户端请求          │
                    └───────────┬─────────────┘
                                │
                                ▼
                    ┌─────────────────────────┐
                    │   ASGI服务器(uvicorn)   │
                    │     事件循环驱动         │
                    │     (单线程)            │
                    └───────────┬─────────────┘
                                │
            ┌───────────────────┼───────────────────┐
            │                   │                   │
            ▼                   ▼                   ▼
    ┌───────────────┐   ┌───────────────┐   ┌───────────────┐
    │  async 函数   │   │  async 函数   │   │  普通 def 函数 │
    │               │   │               │   │               │
    │ 事件循环直接   │   │ 事件循环直接   │   │    ↓          │
    │ 调度执行      │   │ 调度执行      │   │  自动放入      │
    └───────────────┘   └───────────────┘   │  线程池执行    │
                                            └───────────────┘

五、为什么async函数里必须用异步库

这是一个很多人踩过的坑,而且踩了之后还不一定知道问题在哪。

5.1 错误示范

python 复制代码
import requests  # 同步库

@app.get('/bad')
async def bad_example():
    # 灾难!requests 是同步库,会阻塞整个事件循环!
    response = requests.get("http://api.com")
    return response.json()

5.2 发生了什么

复制代码
事件循环(只有1个线程):

时间线 ──────────────────────────────────────────────────────>

请求A: |--执行--|=========同步请求阻塞========|--返回--|
请求B:          被卡住,动不了...
请求C:          被卡住,动不了...
请求D:          被卡住,动不了...

问题:同步库 requests 把唯一的事件循环线程卡死了!
所有其他请求都得排队等!

为什么会这样?因为 requests.get() 是同步的,它不会说"我要等东西,你先忙别的去",它就是死等,把整个线程卡住。

FastAPI 的事件循环就跑在这一个线程上,线程被卡住了,事件循环也就停了。

5.3 正确示范

python 复制代码
import httpx  # 异步库

@app.get('/good')
async def good_example():
    # 正确!httpx 是异步库,等待时会让出控制权
    async with httpx.AsyncClient() as client:
        response = await client.get("http://api.com")
    return response.json()

5.4 发生了什么

复制代码
事件循环(还是只有1个线程):

请求A: |执行| ------await等待------ |拿到结果|返回|
请求B:       |执行| ---await等待--- |结果|返回|
请求C:             |执行| ---await等待--- |结果|返回|

关键:await 的时候,控制权交还给事件循环
事件循环就可以去处理其他请求了!

5.5 核心理解

复制代码
┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  事件循环只有 1 个线程在跑                                    │
│                                                              │
│  同步库 = 你霸占着这个线程傻等,别人都得等你                    │
│  异步库 = 你说"我要等东西,但我先让出来,好了叫我"              │
│                                                              │
│  所以:async 函数里用同步库 = 伪异步 = 性能灾难!               │
│                                                              │
└──────────────────────────────────────────────────────────────┘

5.6 常见同步库与异步库对照

同步库 异步库 用途
requests httpx / aiohttp HTTP请求
psycopg2 asyncpg PostgreSQL
PyMySQL aiomysql MySQL
redis-py aioredis / redis.asyncio Redis
time.sleep() asyncio.sleep() 延时
open() aiofiles 文件操作

六、FastAPI的线程池是干嘛的

你可能注意到了,FastAPI 里可以写普通的 def 函数,不一定要 async def

python 复制代码
@app.get('/sync')
def sync_endpoint():  # 没有 async
    time.sleep(1)     # 同步阻塞操作
    return {"type": "sync"}

咦?这样不会阻塞事件循环吗?

6.1 FastAPI的聪明之处

FastAPI 会检测你的函数类型:

  • 如果是 async def → 直接在事件循环里执行
  • 如果是普通 def自动扔到线程池里执行

这样普通 def 函数就不会阻塞事件循环了。

内部实现大概是这样:

python 复制代码
from concurrent.futures import ThreadPoolExecutor

线程池 = ThreadPoolExecutor(max_workers=40)

async def 处理请求(你的函数, 参数):
    if 是异步函数(你的函数):
        return await 你的函数(参数)
    else:
        # 把同步函数扔到线程池,不阻塞事件循环
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(线程池, 你的函数, 参数)

6.2 什么时候用async def vs def

场景 推荐写法 原因
函数里有异步I/O操作 async def 能充分利用异步的优势
CPU密集型计算 普通 def 会自动放到线程池,不阻塞事件循环
必须用同步库 普通 def 同上
没有任何I/O,纯内存操作 都行 async def 稍微有点开销,但影响不大
python 复制代码
# 用 async def:当你的函数里有异步I/O操作
@app.get('/async-io')
async def async_io():
    async with httpx.AsyncClient() as client:
        data = await client.get("http://api.com")
    return data.json()

# 用普通 def:当你的函数是CPU密集型
@app.get('/cpu-heavy')
def cpu_heavy():
    result = 复杂计算()  # CPU密集,没有I/O
    return {"result": result}

# 用普通 def:当你必须使用同步库
@app.get('/legacy')
def use_legacy_lib():
    data = 某个只有同步版本的老库.查询()
    return {"data": data}

七、Flask为什么不能直接加async

有人可能会问:Python 的 async 是语言自带的关键字,为什么 Flask 不能用?

7.1 技术上可以写,但没用

python 复制代码
from flask import Flask
app = Flask(__name__)

@app.route('/test')
async def test():     # 语法上可以加 async
    await asyncio.sleep(1)
    return "hello"

7.2 但Flask底层会这样调用你的函数

python 复制代码
# Flask 内部(简化)
result = asyncio.run(test())  # 同步地等待异步函数完成

等于说:Flask 把你的异步函数又变成同步的了,完全没享受到异步的好处。

7.3 根本原因:协议不同

框架 协议 设计时间 特点
Flask WSGI 2003年 同步协议,那时Python还没有async
FastAPI ASGI 2018年 异步协议,原生支持async/await
复制代码
【WSGI - Flask 底层】

请求进来
    ↓
分配一个线程给这个请求
    ↓
调用你的视图函数(同步等待返回)
    ↓
返回响应
    ↓
释放线程

就算你写 async,框架底层也是同步调用的


【ASGI - FastAPI 底层】

请求进来
    ↓
事件循环接收请求,创建协程
    ↓
执行你的 async 视图函数
    ↓
遇到 await → 暂停当前协程,去处理其他请求
    ↓
await 的东西好了 → 回来继续执行
    ↓
返回响应

真正的异步,一个线程处理成千上万请求

八、性能对比与选型建议

8.1 资源消耗对比

复制代码
【处理 10000 个并发连接】

Flask(多线程/多进程):
├── 假设每个连接一个线程
├── 每个线程约 8MB 内存(栈空间)
├── 10000 连接 → 需要大量线程
├── 线程切换有操作系统开销
└── 内存消耗:可能几个GB

FastAPI(协程):
├── 单线程 + 事件循环
├── 每个协程约几KB
├── 10000 连接 → 10000 个协程
├── 协程切换是用户态,几乎无开销
└── 内存消耗:几十MB

差距:可达 100 倍!

当然,实际场景中不会这么极端,但协程的资源优势是实实在在的。

8.2 什么时候选Flask

场景 说明
项目简单,不需要高并发 Flask 上手简单,生态成熟
团队熟悉 Flask 生态 换框架有学习成本
需要用很多只有同步版本的库 异步库生态还没那么全
主要是 CPU 密集型任务 这种情况异步优势不大
维护老项目 重构成本太高

8.3 什么时候选FastAPI

场景 说明
需要高并发、高性能 协程的优势场景
I/O 密集型应用 大量数据库查询、外部 API 调用
构建 API 服务 自动生成文档(Swagger/OpenAPI)
想用现代 Python 特性 类型提示、async/await
新项目,可以选择异步库 生态在逐渐完善

8.4 性能排序(I/O密集型场景)

复制代码
高 ─────────────────────────────────────────────────> 低

FastAPI      Flask+gevent    Flask多线程     Flask单线程
(原生异步)    (协程补丁)       (受GIL限制)     (一次一个)

九、总结

9.1 Python并发的完整图景

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    Python 并发模型全景图                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  GIL 的存在,决定了 Python 的并发特点:                          │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  执行 Python 代码时:                                    │   │
│  │  └── 同一时刻只有 1 个线程能执行(假并行)                │   │
│  │                                                         │   │
│  │  等待 I/O 时:                                          │   │
│  │  └── 多个线程可以同时等待(真并发)                      │   │
│  │  └── 因为等待不需要执行代码,GIL 可以释放                │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  基于这个现实,有两种并发策略:                                  │
│                                                                 │
│  【策略一:多线程】Flask 的方式                                  │
│  ├── 用多个线程,利用"I/O 等待时可以真并发"的特点                │
│  ├── 代价:线程重,内存占用大,切换开销大                        │
│  └── 本质:用重量级工具做轻量级的事                              │
│                                                                 │
│  【策略二:协程】FastAPI 的方式                                  │
│  ├── 单线程 + 事件循环 + 协程                                   │
│  ├── 代价:极小,协程只有几 KB                                  │
│  └── 本质:既然反正不能真并行,那就用最轻的方式做并发             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

9.2 核心要点速记

要点 说明
GIL 让多线程不是"真并行" 但 I/O 等待时可以真并发
Flask 用多线程处理并发 每个请求占一个线程,代价大
FastAPI 用协程处理并发 单线程处理万级连接,代价小
async 函数里必须用异步库 否则会阻塞事件循环
FastAPI 的普通 def 函数 会自动放到线程池,避免阻塞
Flask 基于 WSGI(同步) FastAPI 基于 ASGI(异步)

9.3 一个最终的类比

复制代码
Flask 多线程 ≈ 雇佣 100 个服务员,每人服务 1 桌客人
               服务员等菜时在傻站着,但人多所以能应付

FastAPI 协程 ≈ 雇佣 1 个超级服务员,同时服务 100 桌
               他不会傻等,等菜时去服务别桌
               效率高,成本低

两种方式都能服务 100 桌客人
但后者只需要 1 个人的工资

9.4 常见问题

问题1:既然 FastAPI 这么好,Flask 还有存在的必要吗?

有。Flask 简单、成熟、生态丰富。很多场景不需要那么高的并发,用 Flask 反而更省事。技术选型要看具体需求,不是越新越好。

问题2:我用 FastAPI,但必须用一个只有同步版本的库,怎么办?

用普通 def 定义路由函数就行,FastAPI 会自动把它放到线程池里执行,不会阻塞事件循环。

问题3:为什么不直接去掉 GIL

这个问题 Python 社区讨论了二十多年。去掉 GIL 会让单线程代码变慢,而且很多 C 扩展库依赖 GIL。Python 3.13 开始实验性地支持无 GIL 模式,但还需要时间让生态适应。


热门专栏推荐

等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持

文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊

希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏

如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟

相关推荐
数据光子几秒前
【YOLO数据集】国内交通信号检测
人工智能·python·安全·yolo·目标检测·目标跟踪
百***78757 分钟前
2026 优化版 GPT-5.2 国内稳定调用指南:API 中转实操与成本优化
开发语言·人工智能·python
Amelia11111120 分钟前
day48
python
小北方城市网23 分钟前
第 6 课:云原生架构终极落地|K8s 全栈编排与高可用架构设计实战
大数据·人工智能·python·云原生·架构·kubernetes·geo
智航GIS27 分钟前
10.1 网站防爬与伪装策略
python
belldeep34 分钟前
python:pyTorch 入门教程
pytorch·python·ai·torch
YJlio35 分钟前
Registry Usage (RU) 学习笔记(15.5):注册表内存占用体检与 Hive 体量分析
服务器·windows·笔记·python·学习·tcp/ip·django
奔波霸的伶俐虫37 分钟前
redisTemplate.opsForList()里面方法怎么用
java·开发语言·数据库·python·sql
longze_744 分钟前
生成式UI与未来AI交互变革
人工智能·python·ai·ai编程·cursor·蓝湖
weixin_438077491 小时前
CS336 Assignment 4 (data): Filtering Language Modeling Data 翻译和实现
人工智能·python·语言模型·自然语言处理