【python_并发】requests vs aiohttp vs httpx:HTTP客户端深度对比与实战

一、背景与问题

在做Python网络请求开发时,常见的HTTP客户端库有:

  • requests:最经典的同步库
  • aiohttp:异步IO专用库
  • httpx.Client:httpx的同步客户端
  • httpx.AsyncClient:httpx的异步客户端

好久没有用异步http请求, 最近在做项目时候经常用到这几个库,所以特意总结一下。

它们之间有什么区别?什么时候用同步,什么时候用异步?异步真的比同步快吗?

本文通过实际代码实验,带你深入理解这四种客户端的本质区别。


二、核心概念对比

2.1 四种客户端的本质区别

类型 是否阻塞 适用场景
requests 同步 阻塞整个线程 单次请求、简单脚本
aiohttp 异步 非阻塞,事件循环 高并发、大量IO操作
httpx.Client 同步 阻塞整个线程 单次请求、兼容旧代码
httpx.AsyncClient 异步 非阻塞,事件循环 高并发、现代项目

核心区别:同步库在执行请求时,整个程序会"停下来等待";异步库在等待响应时,会切换去执行其他任务。

2.2 同步 vs 异步原理图解

复制代码
同步执行(串行):
请求1 → 等待 → 响应1 → 请求2 → 等待 → 响应2 → 请求3 → 等待 → 响应3
总耗时 = 单次耗时 × 请求数量

异步执行(并发):
请求1 → 等待1 ──────────────────→ 响应1
请求2 → 等待2 ──────────→ 响应2
请求3 → 等待3 → 响应3
总耗时 ≈ 单次耗时(并发时)

2.3 同步阻塞 vs 异步非阻塞的本质

这是理解同步和异步最关键的一点:

特性 同步库 异步库
执行方式 阻塞线程/事件循环 非阻塞,立即返回
等待时做什么 线程停下来等待 切换去执行其他任务
CPU利用率 等待时CPU空闲 等待时CPU可执行其他代码

什么是阻塞?

python 复制代码
# 同步库(requests)执行流程
def fetch():
    response = requests.get(url)  # ← 程序在这里停下来,什么都不做
    # 线程被"卡住",等待网络响应
    # 这段时间CPU空转,无法执行其他代码
    return response.json()

调用 requests.get() 时,整个线程被阻塞,程序"卡"在那里等待网络响应。等待期间,线程无法做任何其他事情。

什么是非阻塞?

python 复制代码
# 异步库执行流程
async def fetch():
    response = await session.get(url)  # ← 发起请求后立即返回控制权
    # 等待期间,事件循环切换去执行其他协程
    # CPU继续工作,不会空转
    return await response.json()

调用异步方法时,立即返回一个协程对象,控制权交还给事件循环。等待期间,事件循环可以去执行其他协程,CPU不会空闲。

事件循环的核心机制

复制代码
事件循环工作流程:
┌─────────────────────────────────────────┐
│  协程A发起请求 → 挂起,等待IO            │
│  ↓ 控制权返回事件循环                     │
│  事件循环切换到协程B → 执行B的代码        │
│  ↓ 协程B也发起请求 → 挂起                │
│  事件循环切换到协程C → ...               │
│  ↓ 某个协程IO完成 → 恢复执行              │
└─────────────────────────────────────────┘

关键陷阱:在async函数里用同步库

python 复制代码
# ❌ 错误示范
async def fetch_data():
    response = requests.get(url)  # 同步库!
    return response.json()

# 看起来是async函数,但requests.get()会阻塞整个事件循环
# 事件循环被卡住,所有其他协程都无法执行!
# 这比纯同步代码更糟糕,因为伪装成了异步

这是新手最容易犯的错误:async函数里用同步库,会阻塞整个事件循环,导致所有协程都无法并发执行。


三、实战对比实验

3.1 测试环境

创建app_server.py先搭建一个模拟fastapi服务,每个请求固定延迟0.5秒:

python 复制代码
# app_server.py
import time
import uvicorn
import asyncio
from fastapi import FastAPI, Query
from pydantic import BaseModel


app = FastAPI(title="测试接口", version="1.0")

# ------------------------------
# 统一响应模型(所有接口返回格式一致)
# ------------------------------
class BaseResponse(BaseModel):
    code: int = 0
    msg: str = "success"
    time: str = "0.5s delay"

# ------------------------------
# GET 无参接口
# ------------------------------
@app.get("/api/get_sync", summary="测试GET接口(无参)", )
def test_api():
    time.sleep(0.5)
    return {
        **BaseResponse().model_dump(),
        "params": False, 
    }


# ------------------------------
# GET 无参接口
# ------------------------------
@app.get("/api/get_test", summary="测试GET接口(无参)", )
async def test_api():
    await asyncio.sleep(0.5)
    return {
        **BaseResponse().model_dump(),
        "params": False, 
    }

# ------------------------------
# GET 带参数接口(强校验)
# ------------------------------
@app.get("/api/get_user", summary="获取用户ID", )
async def test_user_id(
    user_id: str = Query(..., min_length=1, description="用户ID不能为空")
):
    await asyncio.sleep(0.5)
    return {
        **BaseResponse().model_dump(),
        "params": True, 
        "userId": user_id
    }

# ------------------------------
# POST 无参接口
# ------------------------------
@app.post("/api/test_post", summary="测试POST接口(无参)")
async def test_user_info():
    await asyncio.sleep(0.5)
    return {
        **BaseResponse().model_dump(),
        "params": False, 
    }

# ------------------------------
# POST 接收JSON参数(标准用法)
# ------------------------------
class UserInfoRequest(BaseModel):
    name: str
    age: str

@app.post("/api/user_info", summary="提交用户信息")
async def get_user_info(req: UserInfoRequest):
    await asyncio.sleep(0.5)
    return {
        **BaseResponse().model_dump(),
        "params": True, 
        "data": req.model_dump()
    }

# 为测试启动的http服务
if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000)

3.2 requests vs aiohttp [单个和批量请求对比]

前提在开启fastapi服务之后创建test_aiohttp_request_batch.py 文件并执行该文件

python 复制代码
# test_aiohttp_request_batch.py
import time
import os
import requests
import aiohttp
import asyncio
from tqdm.asyncio import tqdm_asyncio  # 👈 异步专用进度条



# 1. 看似异步,里面其实是同步请求
async def test_faker_async_requests(url, headers, params):
    """虚假的异步 - async函数里用同步requests,会阻塞事件循环,不支持真正的并发"""
    response = requests.post(url, headers=headers, json=params)
    return response.json()

# 2. 同步方法
def test_sync_requests(url, headers, params):
    """同步请求函数 - 需要配合线程池才能实现并发"""
    response = requests.post(url, headers=headers, json=params)
    return response.json()

# 用 asyncio.to_thread 包装同步函数实现并发(下策,能用异步优先用异步)
async def test_to_thread_requests(url, headers, params):
    """通过线程池执行同步请求,实现真正的并发"""
    res = await asyncio.to_thread(test_sync_requests, url, headers, params)          # 多线程,调用同步函数
    return res

# 3. 真正的异步
async def test_aiohttp(url, headers, params):
    """真正的异步请求,对异步并发友好。注意:每次调用都新建session(仅用于演示)"""
    async with aiohttp.ClientSession() as session:                                                                                                                        
        async with session.post(url, headers=headers, json=params) as response:                                                                                           
            return await response.json()   

# 单个请求任务
async def fetch_one(semaphore, fetch_func, base_url: str, headers: dict, params: dict, request_id: int):
    # 信号量加锁,控制并发
    async with semaphore:
        request_start_time = time.time()
        try:
            result = await fetch_func(base_url, headers, params)
            request_cost_time = time.time() - request_start_time
            return {
                "request_id": request_id,
                "status": "success",
                "result": result,
                "cost_time": request_cost_time
            }
        except Exception as e:
            request_cost_time = time.time() - request_start_time
            return {
                "request_id": request_id,
                "status": "error",
                "result": str(e),
                "cost_time": request_cost_time
            }

async def test_concurrent(
    fetch_func,                 # 测试的异步方法名称
    total_requests: int = 100,  # 总请求次数
    max_concurrent: int = 10    # 最大并发数(信号量控制)
):
    """
    高并发请求函数
    - 带信号量控制并发
    - 自动连接池
    - 异常捕获
    - 批量请求
    """
    # 基础配置
    base_url = "http://127.0.0.1:8000/api/user_info"
    headers = {
        "User-Agent": f"PythonClient/2.0 (PID:{os.getpid()})",
        "Accept": "application/json",
    }
    params = {
        "name": "jordan",
        "age": "50",
    }
    # 信号量:限制最大协程并发数(核心)
    semaphore = asyncio.Semaphore(max_concurrent)
    method_start_time = time.time()
    # 创建所有任务
    tasks = [fetch_one(semaphore, fetch_func, base_url, headers, params, i) for i in range(total_requests)]
    # 利用单线程多协程并发执行
    results = await tqdm_asyncio.gather(*tasks)
    method_cost_time = time.time() - method_start_time
    # 统计结果
    success_results = [r for r in results if r["status"] == "success"]
    error_results = [r for r in results if r["status"] == "error"]
    print("\n" + "=" * 50)
    print(f"测试结果统计 并发数: {max_concurrent}")
    print("=" * 50)
    print(f"执行方法:    {fetch_func.__name__}")
    print(f"总请求数:    {total_requests}")
    print(f"成功数:      {len(success_results)}")
    print(f"失败数:      {len(error_results)}")
    print(f"总耗时:      {method_cost_time:.2f}s")
    if success_results:
        elapsed_times = [r["cost_time"] for r in success_results]
        print(f"各请求耗时:  {[round(t, 2) for t in elapsed_times]}s")
        print(f"平均响应时间: {sum(elapsed_times)/len(elapsed_times):.2f}s")
        print(f"最快响应时间: {min(elapsed_times):.2f}s")
        print(f"最慢响应时间: {max(elapsed_times):.2f}s")
    if error_results:
        print("\n失败详情:")
        for r in error_results:
            print(f"  请求 #{r['request_id']}: {r['result']}")
    print("\n成功响应示例:")
    for r in success_results[:2]:
        print(f"  请求 #{r['request_id']} ({r['cost_time']:.2f}s): {r['result']}")
    return results

# ==================
# 运行
# ==================
async def test_main():
    # COUNT = 10
    target_url = "http://127.0.0.1:8000/api/user_info"
    params = {
        "name": "jordan",
        "age": "50",
    }
    # 单次请求测试 -------------------------------------------------------------------------------------------
    print("===== 虚假异步 requests(会阻塞事件循环) =====")
    requests_response = await test_faker_async_requests(target_url, {}, params)
    print(f"requests_response: {requests_response}")

    print("===== 线程池包装 requests =====")
    thread_response = await test_to_thread_requests(target_url, {}, params)
    print(f"thread_response: {thread_response}")

    print("===== 真正异步 aiohttp =====")
    aiohttp_response = await test_aiohttp(target_url, {}, params)
    print(f"aiohttp_response: {aiohttp_response}")

    # 协程异步并发调用请求测试 -------------------------------------------------------------------------------------------
    print("\n===== 虚假异步并发测试(实际串行,阻塞事件循环) =====")
    faker_batch_res = await test_concurrent(test_faker_async_requests, total_requests=5, max_concurrent=5)  # 耗时 2.5秒左右

    print("\n===== 线程池包装并发测试(真正并发) =====")
    thread_batch_res = await test_concurrent(test_to_thread_requests, total_requests=5, max_concurrent=5)   # 耗时 0.5秒左右

    print("\n===== 真正异步并发测试 =====")
    aiohttp_batch_res = await test_concurrent(test_aiohttp, total_requests=5, max_concurrent=5)             # 耗时 0.5秒左右

# 针对post请求, 针对同步请求request, 和异步请求aiohttp的测试
if __name__ == "__main__":
    asyncio.run(test_main())

运行结果: [这里我只贴出并发的关键结果,单次调用的结果基本上是没有区别的]

复制代码
===== 同步 requests =====
requests 总耗时:5.12s  # 串行执行,10 × 0.5秒

===== 异步 aiohttp =====
aiohttp 总耗时:0.56s  # 并发执行,约0.5秒

==================================================
测试结果统计 并发数: 5
==================================================
执行方法:    test_requests
总请求数:    5
成功数:      5
失败数:      0
总耗时:      2.53s
各请求耗时:  [0.5, 0.5, 0.5, 0.5, 0.5]s
平均响应时间: 0.50s
最快响应时间: 0.50s
最慢响应时间: 0.50s

==================================================
测试结果统计 并发数: 5
==================================================
执行方法:    test_to_thread_requests
总请求数:    5
成功数:      5
失败数:      0
总耗时:      0.51s
各请求耗时:  [0.51, 0.51, 0.51, 0.51, 0.51]s
平均响应时间: 0.51s
最快响应时间: 0.51s
最慢响应时间: 0.51s

==================================================
测试结果统计 并发数: 5
==================================================
执行方法:    test_aiohttp
总请求数:    5
成功数:      5
失败数:      0
总耗时:      0.51s
各请求耗时:  [0.5, 0.51, 0.5, 0.5, 0.51]s
平均响应时间: 0.50s
最快响应时间: 0.50s
最慢响应时间: 0.51s

▎ 到这里有人可能会问,为什么同步方法中 各请求耗时: [0.5, 0.5, 0.5, 0.5, 0.5]s 这个同步并发调用的每个请求结果都是 0.5s 左右呢?

▎ 这是因为 cost_time 只记录了该请求本身的执行耗时,从请求发起到响应返回的时间。由于request

是同步阻塞调用,会阻塞整个事件循环,实际上请求是串行执行的:请求1完成后请求2才开始,请求2完成后请求3才开始...

▎ 所以虽然每个请求的 cost_time 都是 0.5s,但总耗时 = 10 × 0.5s = 5s,这才是串行执行的真实体现。Semaphore 在这里只是控制协程进入的数量,但由于同步调用阻塞了事件循环,无法实现真正的并发。 于是就有了这个结果,这里主要看总耗时就可以明白了。

总结:虽然用了 Semaphore,但同步调用会阻塞事件循环【线程】,请求还是一个接一个执行的。

3.2.1 补充:asyncio.to_thread 的原理

上面的代码展示了三种并发请求方式,其中 test_to_thread_requests 使用了 asyncio.to_thread 包装同步函数,实现了真正的并发。那么 asyncio.to_thread 是什么?

什么是 asyncio.to_thread?

asyncio.to_thread 是 Python 3.9+ 提供的函数,它将同步函数放到独立线程中执行,让事件循环可以继续处理其他协程。

python 复制代码
# asyncio.to_thread 的工作原理
async def test_to_thread_requests(url, headers, params):
    # 将同步函数 test_sync_requests 放到线程池执行
    # 事件循环不会被阻塞,可以继续执行其他协程
    res = await asyncio.to_thread(test_sync_requests, url, headers, params)
    return res

执行流程对比

复制代码
虚假异步(test_faker_async_requests):
┌─────────────────────────────────────────┐
│ 协程1调用requests → 阻塞事件循环         │
│ ↓ 所有协程都在等,无法并发               │
│ 协程2等待... 协程3等待...                │
│ 串行执行,总耗时 = N × 单次耗时          │
└─────────────────────────────────────────┘

asyncio.to_thread(test_to_thread_requests):
┌─────────────────────────────────────────┐
│ 协程1 → to_thread → 线程1执行requests    │
│ ↓ 事件循环继续,不被阻塞                 │
│ 协程2 → to_thread → 线程2执行requests    │
│ ↓ 多个线程并行执行                       │
│ 真正并发,总耗时 ≈ 单次耗时              │
└─────────────────────────────────────────┘

真正异步(test_aiohttp):
┌─────────────────────────────────────────┐
│ 协程1 → aiohttp发起请求 → 挂起等待IO     │
│ ↓ 事件循环切换执行其他协程               │
│ 协程2 → aiohttp发起请求 → 挂起等待IO     │
│ ↓ 单线程内多协程并发,无需额外线程       │
│ 真正并发,总耗时 ≈ 单次耗时              │
└─────────────────────────────────────────┘

三种方式的本质区别

方式 执行位置 是否阻塞事件循环 是否并发
虚假异步(async函数里直接调用同步库) 主线程 阻塞 ❌ 串行
asyncio.to_thread 独立线程 不阻塞 ✓ 并发
真正异步(aiohttp) 主线程(协程挂起) 不阻塞 ✓ 并发

为什么 asyncio.to_thread 不阻塞主线程?

这是理解 asyncio.to_thread 的关键点:

核心原理

线程 阻塞情况 影响
主线程(事件循环) 不阻塞 可以继续调度其他协程
子线程(线程池) 阻塞 但不影响主线程,子线程之间并行

所以:

  • requests.get() 依然是阻塞调用
  • 但它阻塞的是子线程,不是主线程
  • 主线程的事件循环继续运转,可以调度其他协程
  • 多个子线程可以并行执行,实现真正的并发

一句话总结asyncio.to_thread 把阻塞甩给子线程,主线程保持自由。

为什么 asyncio.to_thread 是"下策"?

python 复制代码
# 下策:用线程池包装同步函数
async def fetch():
    res = await asyncio.to_thread(requests.get, url)  # 需要额外线程
    return res.json()

# 上策:直接用异步库
async def fetch():
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()  # 无需额外线程,更高效
对比项 asyncio.to_thread 真正异步(aiohttp)
线程开销 需要创建线程 不需要,协程足够轻量
内存占用 每个线程~8MB 每个协程~几KB
最大并发数 受线程数限制(通常几十到几百) 几千甚至上万
适用场景 无法修改的同步代码、遗留代码 新项目、高并发场景

什么时候用 asyncio.to_thread?

  • 无法修改的遗留同步代码
  • 第三方库只有同步接口,没有异步版本
  • 快速验证并发效果,后续再迁移到异步库

总结 :能用异步库就优先用异步库,asyncio.to_thread 是不得已的下策。


3.3 httpx同步 vs 异步对比 [单个和批量请求对比]

前提在开启fastapi服务之后创建test_http_client_batch.py 文件并执行该文件

python 复制代码
# 04_test_http_client_batch.py
import os
import time
import httpx
import asyncio
from loguru import logger
from typing import Dict
from tqdm.asyncio import tqdm_asyncio  # 👈 异步专用进度条

async def test_sync_httpx(base_url, headers, params):
    """测试同步请求方法, 阻塞线程"""
    with httpx.Client(base_url=base_url, headers=headers, timeout=5) as sync_client: 
        params_response = sync_client.request(
            "post",
            f"/api/user_info",
            json=params,
        )
        return params_response.json()

async def test_async_httpx(base_url, headers, params):
    """异步调用, 非阻塞线程正确的异步调用方法"""
    # 异步调用的客户短
    async with httpx.AsyncClient(base_url=base_url, headers=headers, timeout=5) as async_client:
        params_response = await async_client.request(
            "post",
            f"/api/user_info",
            json=params,
        )       
        return params_response.json()


# 单个请求任务
async def fetch_one(semaphore, fetch_func, base_url: str, headers: dict, params: dict, request_id: int):
    # 信号量加锁,控制并发
    async with semaphore:
        request_start_time = time.time()
        try:
            result = await fetch_func(base_url, headers, params)
            request_cost_time = time.time() - request_start_time
            return {
                "request_id": request_id,
                "status": "success",
                "result": result,
                "cost_time": request_cost_time
            }
        except Exception as e:
            request_cost_time = time.time() - request_start_time
            return {
                "request_id": request_id,
                "status": "error",
                "result": str(e),
                "cost_time": request_cost_time
            }

async def test_concurrent(
    fetch_func,                 # 测试的异步方法名称
    total_requests: int = 100,  # 总请求次数
    max_concurrent: int = 10    # 最大并发数(信号量控制)
):
    """
    高并发请求函数
    - 带信号量控制并发
    - 自动连接池
    - 异常捕获
    - 批量请求
    """
    # 基础配置
    base_url = "http://127.0.0.1:8000"
    headers = {
        "User-Agent": f"PythonClient/2.0 (PID:{os.getpid()})",
        "Accept": "application/json",
    }
    params = {
        "name": "jordan",
        "age": "50",
    }
    # 信号量:限制最大协程并发数(核心) 
    semaphore = asyncio.Semaphore(max_concurrent)
    method_start_time = time.time()
    # 创建所有任务
    tasks = [fetch_one(semaphore, fetch_func, base_url, headers, params, i) for i in range(total_requests)]
    # 利用单线程,多协程并发执行
    results = await tqdm_asyncio.gather(*tasks)
    method_cost_time = time.time() - method_start_time
    # 统计结果
    success_results = [r for r in results if r["status"] == "success"]
    error_results = [r for r in results if r["status"] == "error"]
    print("\n" + "=" * 50)
    print(f"测试结果统计 并发数: {max_concurrent}")
    print("=" * 50)
    print(f"执行方法:    {fetch_func.__name__}")
    print(f"总请求数:    {total_requests}")
    print(f"成功数:      {len(success_results)}")
    print(f"失败数:      {len(error_results)}")
    print(f"总耗时:      {method_cost_time:.2f}s")
    if success_results:
        elapsed_times = [r["cost_time"] for r in success_results]
        print(f"各请求耗时:  {[round(t, 2) for t in elapsed_times]}s")
        print(f"平均响应时间: {sum(elapsed_times)/len(elapsed_times):.2f}s")
        print(f"最快响应时间: {min(elapsed_times):.2f}s")
        print(f"最慢响应时间: {max(elapsed_times):.2f}s")
    if error_results:
        print("\n失败详情:")
        for r in error_results:
            print(f"  请求 #{r['request_id']}: {r['result']}")
    print("\n成功响应示例:")
    for r in success_results[:2]:
        print(f"  请求 #{r['request_id']} ({r['cost_time']:.2f}s): {r['result']}")
    return results

async def main():
    base_url = "http://127.0.0.1:8000"
    headers={
            "User-Agent": f"PythonClient/2.0 (PID:{os.getpid()})",
            "Accept": "application/json",
            # "Authorization": "Bearer " + cls._secret,                 # 如果要进行token验证
        }
    # 同步调用的客户端
    params = {
        "name": "jordan",
        "age": "50",
    }
    # 单次请求测试 -------------------------------------------------------------------------------------------
    print("===== 同步 requests =====")
    requests_response = await test_sync_httpx(base_url, headers, params)       # 
    print(f"requests_response: {requests_response}")

    print("===== 异步 aiohttp =====")
    aiohttp_response =  await test_async_httpx(base_url, headers, params)
    print(f"aiohttp_response: {aiohttp_response}")

    sync_httpx_res = await test_concurrent(test_sync_httpx,  total_requests=10, max_concurrent=5)      # 耗时 5秒左右
    # print(f"sync_httpx_res: {sync_httpx_res}")
    async_httpx_res = await test_concurrent(test_async_httpx, total_requests=10, max_concurrent=5)      # 耗时 1秒左右
    # print(f"async_httpx_res: {async_httpx_res}")

# 运行方式
# 测试Semaphore针对http.client,http.AsyncClient针对异步的并发支持 效果
if __name__ == "__main__":
    # 发 50 个请求,最大同时并发 10
    asyncio.run(main())

运行结果: [这里我只贴出并发的关键结果,单次调用的结果基本上是没有区别的]

复制代码
==================================================
测试结果统计 并发数: 5
==================================================
执行方法:    test_sync_httpx
总请求数:    10
成功数:      10
失败数:      0
总耗时:      5.57s
各请求耗时:  [0.55, 0.56, 0.54, 0.56, 0.56, 0.56, 0.56, 0.56, 0.55, 0.56]s
平均响应时间: 0.56s
最快响应时间: 0.54s
最慢响应时间: 0.56s


==================================================
测试结果统计 并发数: 5
==================================================
执行方法:    test_async_httpx
总请求数:    10
成功数:      10
失败数:      0
总耗时:      1.32s
各请求耗时:  [0.64, 0.66, 0.66, 0.62, 0.59, 0.57, 0.69, 0.67, 0.54, 0.58]s
平均响应时间: 0.62s
最快响应时间: 0.54s
最慢响应时间: 0.69s

关键点

  • httpx.Client = 同步,阻塞线程,和requests类似
  • httpx.AsyncClient = 异步,非阻塞,和aiohttp类似

▎ 到这里有人可能会问,为什么 各请求耗时: [0.55, 0.56, 0.54, 0.56, 0.56, 0.56, 0.56, 0.56, 0.55, 0.56]s 这个同步并发调用的每个请求结果都是 0.5s 左右呢?

▎ 这是因为 cost_time 只记录了该请求本身的执行耗时,从请求发起到响应返回的时间。由于 httpx.Client

是同步阻塞调用,会阻塞整个事件循环,实际上请求是串行执行的:请求1完成后请求2才开始,请求2完成后请求3才开始...

▎ 所以虽然每个请求的 cost_time 都是 0.5s,但总耗时 = 10 × 0.5s = 5s,这才是串行执行的真实体现。Semaphore 在这里只是控制协程进入的数量,但由于同步调用阻塞了事件循环,无法实现真正的并发。 于是就有了这个结果,这里主要看总耗时就可以明白了。

总结:虽然用了 Semaphore,但同步调用会阻塞事件循环【线程】,请求还是一个接一个执行的。

四、资源管理最佳实践

4.1 错误写法(资源泄漏)

python 复制代码
# ❌ 错误:每次请求创建session但不关闭
async def test_aiohttp(url, headers, params):
    session = aiohttp.ClientSession()  # 没关闭!
    response = await session.post(url, json=params)
    return await response.json()  # 如果这里异常,session永远不关闭

# ❌ 错误:httpx client 不关闭
async_client = httpx.AsyncClient(...)
response = await async_client.get(url)  # 不关闭会泄漏连接

4.2 正确写法对比

方式一:上下文管理器(推荐)

python 复制代码
# aiohttp 正确写法
async with aiohttp.ClientSession() as session:
    async with session.post(url, json=params) as response:
        return await response.json()

# httpx.AsyncClient 正确写法
async with httpx.AsyncClient(base_url=url, timeout=5) as client:
    async with client.post("/api/user_info", json=params) as response:
        return await response.json()

方式二:try-finally手动关闭

python 复制代码
# aiohttp 手动关闭
session = aiohttp.ClientSession()
try:
    response = await session.post(url, json=params)
    return await response.json()
finally:
    await session.close()

# httpx.AsyncClient 手动关闭
async_client = httpx.AsyncClient(base_url=url, timeout=5)
try:
    response = await async_client.post("/api/user_info", json=params)
    return await response.json()
finally:
    await async_client.aclose()  # 注意是aclose,不是close

4.3 关闭方法对照表

同步关闭 异步关闭
aiohttp.ClientSession - await session.close()
httpx.Client client.close() -
httpx.AsyncClient - await client.aclose()

注意 :AsyncClient的异步关闭方法是 aclose(),不是 close()


五、使用场景总结

5.1 什么时候用同步?

  • 单次请求:简单脚本、配置文件读取
  • 非IO密集:计算密集型任务
  • 兼容旧代码:迁移成本考虑
  • 调试阶段:同步代码更容易调试
python 复制代码
# 简单脚本用 requests 最方便
import requests
response = requests.get("https://api.example.com/data")
print(response.json())

5.2 什么时候用异步?

  • 高并发请求:批量API调用、爬虫
  • IO密集型:大量网络/文件IO等待
  • 实时应用:WebSocket、长连接
  • 微服务架构:多个下游服务调用
python 复制代码
# 高并发场景用异步
async def batch_request(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [await r.json() for r in responses]

5.3 httpx vs requests vs aiohttp 选择

场景 推荐库 原因
快速原型开发 requests API简单,文档丰富
高并发爬虫 aiohttp 异步性能最优
现代项目(同步+异步都支持) httpx 一套API两种模式,迁移方便
FastAPI/现代异步框架 httpx.AsyncClient 和框架风格统一

六、常见错误与注意事项

6.1 async函数中使用同步库

python 复制代码
# ❌ 错误:async函数里用同步库
async def fetch_data():
    response = requests.get(url)  # 阻塞整个事件循环!
    return response.json()

# ✓ 正确:用对应的异步库
async def fetch_data():
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.json()

6.2 忘记await

python 复制代码
# ❌ 错误:忘记await
async def fetch():
    async with aiohttp.ClientSession() as session:
        response = session.get(url)  # 没await,得到的是coroutine对象
        return response.json()  # 报错!

# ✓ 正确
async def fetch():
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:  # 加await/async with
            return await response.json()

6.3 高并发时每个请求创建新session

python 复制代码
# ❌ 效率低:每次请求新建session
async def fetch_one(url):
    async with aiohttp.ClientSession() as session:  # 每次新建
        async with session.get(url) as response:
            return await response.json()

# ✓ 推荐:复用session
async def batch_fetch(urls):
    async with aiohttp.ClientSession() as session:  # 只创建一次
        tasks = [session.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return responses

七、性能对比总结

场景 同步耗时 异步耗时 提升
10个请求(串行) ~5秒 ~0.5秒 10倍
100个请求(并发10) ~50秒 ~5秒 10倍
单次请求 ~0.5秒 ~0.5秒 无差异

结论

  • 单次请求:同步和异步性能相同
  • 批量请求:异步性能提升N倍(N为并发数)
  • 异步的优势在于等待IO时可以做其他事,不是单次请求更快

参考资料


博客为个人学习笔记,如有错误,请指正修改。

相关推荐
gCode Teacher 格码致知2 小时前
Python基础教学:正则表达式中的忽略大小写以及符号“-“的问题-由Deepseek产生
python·正则表达式
斯班奇的好朋友阿法法2 小时前
Django 项目打包部署完整指南(适配你的项目,零报错)
python·django·sqlite
林开落L2 小时前
【项目实战】博客系统完整测试报告(含自动化+性能测试)
python·功能测试·jmeter·自动化·postman·性能测试·xmind
JustNow_Man2 小时前
【opencode】使用方法
linux·服务器·网络·人工智能·python
abigale032 小时前
.py 与 .ipynb 的核心差异 + Jupyter 内核缓存坑全解析
python·jupyter
Dxy12393102162 小时前
Python使用SymSpell详解:打造极速拼写检查引擎
开发语言·python
AI_Claude_code2 小时前
网络基础回顾:DNS、IP封锁与HTTP/S协议关键点
网络·爬虫·python·tcp/ip·http·爬山算法·安全架构
架构师老Y2 小时前
012、缓存架构设计:Redis高级应用与优化
redis·python·架构
Thomas.Sir2 小时前
AI 医疗之重症监护预警系统(ICU-EWS)从理论到实战【时序深度学习与多模态融合】
人工智能·python·深度学习·ai·多模态