Python FastAPI 和 Uvicorn 同步 (Synchronous) vs 异步 (Asynchronous)

Python FastAPI 和 Uvicorn 同步 (Synchronous) vs 异步 (Asynchronous)

flyfish

FastAPI 是框架 (framework):它提供工具来定义 API 的结构、路由和逻辑,但本身不处理底层网络服务器的功能。它描述了应用如何响应请求,但不启动监听。

Uvicorn 是服务器 (server):它负责运行框架定义的应用,处理实际的 HTTP/TCP 连接、请求队列、多并发等底层细节。

FastAPI 依赖 ASGI 服务器来运行,而 Uvicorn 是最常用的一个。

FastAPI 生成一个 ASGI 兼容的应用对象(app),Uvicorn 通过 uvicorn.run(app) 加载这个对象,启动事件循环,监听端口并将请求转发给 FastAPI 处理。 在异步场景中,Uvicorn 的 uvloop 增强了 FastAPI 的 asyncio 性能。

FastAPI 提供异步路由支持,但 Uvicorn 确保服务器层面的并发(如多 worker 处理阻塞任务)。FastAPI 只是一个框架,它定义了应用的逻辑,但无法独立运行服务器功能,没有 Uvicorn(或类似服务器),FastAPI 应用无法监听端口、接受 HTTP 请求或启动事件循环。

服务器端代码 同步 (Synchronous)

cpp 复制代码
# main.py

# 1. 导入 FastAPI
from fastapi import FastAPI

# 2. 创建一个 FastAPI 实例
app = FastAPI(
    title="FastAPI 应用",
    description="这是一个非常简单的 FastAPI 示例。",
    version="1.0.0",
)

# 3. 定义一个路径操作(Path Operation)
# @app.get("/") 是一个装饰器,它告诉 FastAPI 下面的函数用来处理访问 "/" 路径的 GET 请求
@app.get("/")
def read_root():
    """
    根路径,返回一个欢迎信息。
    """
    return {"message": "欢迎来到 FastAPI 世界!"}

# 定义另一个路径操作,它接收一个路径参数
@app.get("/hello/{name}")
def say_hello(name: str):
    """
    路径参数 `name` 会被自动获取并传递给函数。
    FastAPI 会自动进行数据校验,如果传入的不是字符串,会返回 422 错误。
    """
    return {"message": f"你好, {name}!"}

# 4. 运行服务器
# 如果这个文件是直接被运行的(而不是被其他脚本导入),
# 那么就启动 Uvicorn 服务器来运行 FastAPI 应用。
if __name__ == "__main__":
    import uvicorn
    # uvicorn.run(app, host="0.0.0.0", port=8000)
    # 使用更简洁的命令行风格启动
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
    # - "main:app" 告诉 Uvicorn 去 main.py 文件中寻找名为 app 的 FastAPI 实例。
    # - host="0.0.0.0" 让服务器可以被局域网内的其他设备访问。
    # - port=8000 指定端口为 8000。
    # - reload=True 让服务器在代码修改后自动重启,非常适合开发阶段。

客户端代码

cpp 复制代码
# client.py

import requests

# 定义服务器的基础 URL
BASE_URL = "http://127.0.0.1:8000"

def test_read_root():
    """测试访问根路径"""
    response = requests.get(f"{BASE_URL}/")
    if response.status_code == 200:
        print("成功访问根路径!")
        print("响应内容:", response.json())
    else:
        print(f"访问失败,状态码: {response.status_code}")

def test_say_hello(name):
    """测试访问带参数的路径"""
    response = requests.get(f"{BASE_URL}/hello/{name}")
    if response.status_code == 200:
        print(f"成功访问 /hello/{name}!")
        print("响应内容:", response.json())
    else:
        print(f"访问失败,状态码: {response.status_code}")

if __name__ == "__main__":
    test_read_root()
    print("-" * 20)
    test_say_hello("Python")

运行结果

cpp 复制代码
成功访问根路径!
响应内容: {'message': '欢迎来到 FastAPI 世界!'}
--------------------
成功访问 /hello/Python!
响应内容: {'message': '你好, Python!'}
(base) PS D:\work\OA\20251219OAfabu\jing(井号,功图存储位置,)\git20251121v1\event_server\test>

异步服务器端代码 (async_server.py)

cpp 复制代码
# async_server.py
import asyncio
import time
from fastapi import FastAPI

app = FastAPI()

# 模拟一个耗时的I/O操作(例如:数据库查询、文件读取)
# 使用 asyncio.sleep() 来模拟非阻塞的等待
async def async_sleep():
    print("Async task started. Simulating 2 seconds of I/O wait...")
    await asyncio.sleep(2)  # 这是一个非阻塞操作
    print("Async task finished.")
    return "Async task result"

# 模拟一个耗时的I/O操作
# 使用 time.sleep() 来模拟阻塞的等待
def sync_sleep():
    print("Sync task started. Simulating 2 seconds of blocking wait...")
    time.sleep(2)  # 这是一个阻塞操作
    print("Sync task finished.")
    return "Sync task result"

# 异步接口
@app.get("/async-endpoint")
async def async_endpoint():
    """
    这个接口是异步的。
    当它执行 await async_sleep() 时,它会释放服务器的工作线程,
    让服务器可以去处理其他请求。
    """
    result = await async_sleep()
    return {"message": "This is an async endpoint.", "result": result}


# @app.get("/sync-endpoint")
# def sync_endpoint():
#     result = sync_sleep()
#     return {"message": "This is a sync endpoint.", "result": result}


# 同步接口 (modified to block the event loop)
@app.get("/sync-endpoint")
async def sync_endpoint():  # Change to 'async def'
    """
    这个接口现在是异步定义的,但内部调用阻塞操作。
    当它执行 time.sleep(2) 时,它会阻塞整个事件循环,
    在这2秒内,服务器无法处理任何其他请求,导致请求串行化。
    """
    result = sync_sleep()  # Call blocking function directly
    return {"message": "This is a sync endpoint.", "result": result}

if __name__ == "__main__":
    import uvicorn
    # 使用 workers=1 可以更清楚地看到单线程下的并发效果
    uvicorn.run("async_server:app", host="0.0.0.0", port=8000, reload=True, workers=1)

仔细看代码,一个同步函数加了async,里面没有await 。阻塞不应该是没有async,也没有await 吗? 这样原因在后边说

异步客户端代码 (async_client.py)

能同时发送多个请求的客户端

cpp 复制代码
# async_client.py
import asyncio
import time
import httpx

# 定义要测试的接口 URL
ASYNC_URL = "http://127.0.0.1:8000/async-endpoint"
SYNC_URL = "http://127.0.0.1:8000/sync-endpoint"

# 并发请求的数量
NUM_REQUESTS = 5

async def fetch_url(client, url, request_num):
    """异步函数,用于发送单个请求"""
    print(f"Request {request_num}: Sending request to {url}")
    response = await client.get(url)
    print(f"Request {request_num}: Received response: {response.json()}")
    return response.json()

async def test_concurrency(url, name):
    """测试指定URL的并发性能"""
    print(f"\n--- Testing {name} Endpoint with {NUM_REQUESTS} concurrent requests ---")
    start_time = time.perf_counter()

    # 创建一个异步的HTTP客户端
    async with httpx.AsyncClient(timeout=httpx.Timeout(20.0)) as client:
        # 创建一个任务列表
        tasks = [fetch_url(client, url, i+1) for i in range(NUM_REQUESTS)]
        # 使用 asyncio.gather 并发执行所有任务
        await asyncio.gather(*tasks)

    end_time = time.perf_counter()
    total_time = end_time - start_time
    print(f"--- All {NUM_REQUESTS} requests completed in {total_time:.2f} seconds ---\n")

async def main():
    # 先测试异步接口
    await test_concurrency(ASYNC_URL, "Async")
    
    # 再测试同步接口
    await test_concurrency(SYNC_URL, "Sync")

if __name__ == "__main__":
    asyncio.run(main())

运行结果

cpp 复制代码
--- Testing Async Endpoint with 5 concurrent requests ---
Request 1: Sending request to http://127.0.0.1:8000/async-endpoint
Request 2: Sending request to http://127.0.0.1:8000/async-endpoint
Request 3: Sending request to http://127.0.0.1:8000/async-endpoint
Request 4: Sending request to http://127.0.0.1:8000/async-endpoint
Request 5: Sending request to http://127.0.0.1:8000/async-endpoint
Request 2: Received response: {'message': 'This is an async endpoint.', 'result': 'Async task result'}
Request 5: Received response: {'message': 'This is an async endpoint.', 'result': 'Async task result'}
Request 1: Received response: {'message': 'This is an async endpoint.', 'result': 'Async task result'}
Request 4: Received response: {'message': 'This is an async endpoint.', 'result': 'Async task result'}
Request 3: Received response: {'message': 'This is an async endpoint.', 'result': 'Async task result'}
--- All 5 requests completed in 2.09 seconds ---


--- Testing Sync Endpoint with 5 concurrent requests ---
Request 1: Sending request to http://127.0.0.1:8000/sync-endpoint
Request 2: Sending request to http://127.0.0.1:8000/sync-endpoint
Request 3: Sending request to http://127.0.0.1:8000/sync-endpoint
Request 4: Sending request to http://127.0.0.1:8000/sync-endpoint
Request 5: Sending request to http://127.0.0.1:8000/sync-endpoint
Request 1: Received response: {'message': 'This is a sync endpoint.', 'result': 'Sync task result'}
Request 4: Received response: {'message': 'This is a sync endpoint.', 'result': 'Sync task result'}
Request 3: Received response: {'message': 'This is a sync endpoint.', 'result': 'Sync task result'}
Request 2: Received response: {'message': 'This is a sync endpoint.', 'result': 'Sync task result'}
Request 5: Received response: {'message': 'This is a sync endpoint.', 'result': 'Sync task result'}
--- All 5 requests completed in 10.04 seconds ---

一个同步函数加了async,里面没有await 。阻塞不应该是没有async,也没有await 吗?

FastAPI 基于 ASGI(Asynchronous Server Gateway Interface)标准,使用异步事件循环(asyncio)来处理请求,以支持高并发 I/O 操作。

同步路由(def)

FastAPI 检测到是同步函数时,会使用内部的线程池执行器(基于 anyio.to_thread.run_sync 或类似机制)将函数"移到"后台线程运行。这防止了同步阻塞代码污染主事件循环。线程池默认大小较大,允许多个同步请求并发执行阻塞操作(如 I/O 或 sleep)。原理:Python 的多线程(threading)可以处理 I/O 绑定任务的并发,即使有 GIL(因为 sleep 会释放 GIL,让其他线程运行)。
我们自己写了同步函数但是FastAPI 把同步函数扔到了线程池里造成线程池并发,5 个请求总耗时 2s,即5个请求被并发执行了,所以不是10S

异步路由(async def)

FastAPI 直接在主事件循环上调度执行。如果内部使用 await(如 asyncio.sleep),事件循环可以"暂停"当前任务,转而处理其他任务,实现真正的非阻塞并发。

但如果直接调用阻塞代码,def sync_endpoint()是一个同步阻塞函数(不是协程),它不能用 await ,事件循环会被"卡住",无法切换任务,导致所有请求队列化。阻塞主循环,所以5 个请求总耗时 10s。

事件循环(Event Loop)

asyncio 的是一个单线程的事件循环。它通过"协作式多任务"(cooperative multitasking)工作:任务必须主动"让出"控制权(via await),循环才能切换到其他任务。

阻塞版本(10s 效果)

python 复制代码
@app.get("/sync-endpoint")
async def sync_endpoint():  # async def
    time.sleep(2)  # 直接调用阻塞,无 await 或线程
    return {"message": "Blocked the event loop"}

阻塞主循环,5 个并发请求总耗时 ≈10s。

非阻塞版本(2s 效果)

python 复制代码
@app.get("/sync-endpoint")
def sync_endpoint():  # def
    time.sleep(2)  # 阻塞,但被线程池处理
    return {"message": "Offloaded to thread pool"}

线程池并发,5 个请求总耗时 ≈2s

上面的阻塞只是用于演示。

相关推荐
Hgfdsaqwr9 小时前
Django全栈开发入门:构建一个博客系统
jvm·数据库·python
开发者小天9 小时前
python中For Loop的用法
java·服务器·python
布局呆星9 小时前
FastAPI:高性能Python Web框架
fastapi
老百姓懂点AI10 小时前
[RAG实战] 向量数据库选型与优化:智能体来了(西南总部)AI agent指挥官的长短期记忆架构设计
python
喵手11 小时前
Python爬虫零基础入门【第九章:实战项目教学·第15节】搜索页采集:关键词队列 + 结果去重 + 反爬友好策略!
爬虫·python·爬虫实战·python爬虫工程化实战·零基础python爬虫教学·搜索页采集·关键词队列
Suchadar12 小时前
if判断语句——Python
开发语言·python
ʚB҉L҉A҉C҉K҉.҉基҉德҉^҉大12 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
喵手12 小时前
Python爬虫零基础入门【第九章:实战项目教学·第14节】表格型页面采集:多列、多行、跨页(通用表格解析)!
爬虫·python·python爬虫实战·python爬虫工程化实战·python爬虫零基础入门·表格型页面采集·通用表格解析
0思必得013 小时前
[Web自动化] 爬虫之API请求
前端·爬虫·python·selenium·自动化
莫问前路漫漫13 小时前
WinMerge v2.16.41 中文绿色版深度解析:文件对比与合并的全能工具
java·开发语言·python·jdk·ai编程