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
上面的阻塞只是用于演示。