大家好,我是何三,80后老猿,独立开发者

在Python生态中,requests库长期占据HTTP客户端的主导地位,但近年来httpx异军突起,凭借其出色的性能表现赢得了众多开发者的青睐。今天我们就来深入探讨httpx比requests快的真正原因,并通过实际代码演示来验证这一结论。

同步与异步的本质区别

想象一下你在一家咖啡厅点单:requests就像是一个固执的服务员,每次只处理一个顾客的订单,必须等这个顾客完全拿到咖啡后才接待下一位;而httpx则像是一个高效的服务员团队,可以同时处理多个顾客的订单,在等待咖啡制作的过程中就能服务其他顾客。

import time
import requests
import httpx
import asyncio

# 同步请求示例
def sync_requests(urls):
    start = time.time()
    for url in urls:
        response = requests.get(url)
        print(f"获取 {url},状态码: {response.status_code}")
    return time.time() - start

# 异步请求示例
async def async_httpx(urls):
    start = time.time()
    async with httpx.AsyncClient() as client:
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        for response in responses:
            print(f"获取 {response.url},状态码: {response.status_code}")
    return time.time() - start

# 测试URL列表
test_urls = [
    "https://www.example.com",
    "https://www.python.org",
    "https://www.deepseek.com",
    "https://httpbin.org/delay/1",
    "https://httpbin.org/delay/2"
]

# 执行同步请求
print("同步requests耗时:", sync_requests(test_urls))

# 执行异步请求
print("异步httpx耗时:", asyncio.run(async_httpx(test_urls)))

运行这段代码你会发现,httpx的异步版本完成所有请求的时间远少于requests的同步版本,特别是当URL中包含延迟较高的端点时(如httpbin.org/delay/2),差异更加明显。

为什么异步更快?深入I/O等待

计算机执行网络请求时,大部分时间都花在了等待网络传输上,CPU实际上是空闲的。同步请求就像是在火车站排队买票,即使前面的人正在查询复杂的行程,你也只能干等着;而异步请求则像是网上购票,提交请求后你可以去做其他事情,等结果返回时再处理。

import httpx
import asyncio

async def fetch_with_deepseek(query):
    async with httpx.AsyncClient() as client:
        # 调用DeepSeek API接口示例
        response = await client.post(
            "https://api.deepseek.com/v1/chat/completions",
            json={
                "model": "deepseek-chat",
                "messages": [{"role": "user", "content": query}]
            },
            headers={"Authorization": "Bearer YOUR_API_KEY"}
        )
        return response.json()

# 同时发起多个DeepSeek API请求
async def multi_deepseek_queries():
    queries = [
        "解释Python中的GIL",
        "如何优化Python性能",
        "异步编程有什么优势"
    ]
    tasks = [fetch_with_deepseek(query) for query in queries]
    return await asyncio.gather(*tasks)

# 执行多个DeepSeek查询
results = asyncio.run(multi_deepseek_queries())
for result in results:
    print(result['choices'][0]['message']['content'])

在这个DeepSeek API调用示例中,异步方式可以同时发起多个查询请求,而不必等待前一个完成,这在需要聚合多个API结果时尤其有用。

requests多线程 vs httpx异步

你可能会想:requests配合多线程不也能实现并发吗?确实可以,但两者的实现机制和资源消耗大不相同。

import requests
import threading
import time
import httpx
import asyncio

# requests多线程方式
def requests_with_threads(urls):
    start = time.time()
    threads = []
    results = []

    def fetch(url):
        response = requests.get(url)
        results.append((url, response.status_code))

    for url in urls:
        thread = threading.Thread(target=fetch, args=(url,))
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()

    for url, status in results:
        print(f"获取 {url},状态码: {status}")

    return time.time() - start

# 测试对比
print("requests多线程耗时:", requests_with_threads(test_urls))
print("httpx异步耗时:", asyncio.run(async_httpx(test_urls)))

虽然多线程requests也能实现并发,但每个线程都需要独立的系统资源,线程切换也有开销。而httpx的异步模型在单个线程内通过事件循环管理所有请求,资源利用率更高,特别是在高并发场景下优势更明显。

正确使用asyncio的最佳实践

要充分发挥httpx的异步优势,必须正确使用asyncio。常见错误包括混用同步代码、不恰当的任务管理等。

# 正确的httpx异步使用方式
async def proper_async_fetch():
    # 使用同一个Client实例
    async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
        # 合理控制并发量
        semaphore = asyncio.Semaphore(5)  # 限制最大并发数为5

        async def limited_get(url):
            async with semaphore:
                response = await client.get(url)
                # 处理响应
                data = response.json() if 'application/json' in response.headers.get('content-type', '') else response.text
                return data

        urls = [f"https://httpbin.org/get?id={i}" for i in range(10)]
        tasks = [limited_get(url) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        for result in results:
            if isinstance(result, Exception):
                print(f"请求失败: {result}")
            else:
                print(f"获取数据: {result.get('args', {}).get('id')}")

# 执行示例
asyncio.run(proper_async_fetch())

这段代码展示了几个最佳实践:使用上下文管理器管理Client实例、通过Semaphore控制并发量、正确处理异常、合理设置超时等。

性能对比的量化分析

为了更客观地比较性能,我们设计一个量化测试:

import statistics
import matplotlib.pyplot as plt

async def benchmark():
    test_url = "https://httpbin.org/delay/1"
    num_requests = 100

    # 测试同步requests
    sync_times = []
    for _ in range(5):
        start = time.time()
        for _ in range(num_requests):
            requests.get(test_url)
        sync_times.append(time.time() - start)

    # 测试多线程requests (10线程)
    thread_times = []
    for _ in range(5):
        start = time.time()
        threads = []
        for _ in range(num_requests):
            thread = threading.Thread(target=requests.get, args=(test_url,))
            thread.start()
            threads.append(thread)

        for thread in threads:
            thread.join()
        thread_times.append(time.time() - start)

    # 测试异步httpx
    async def run_async():
        async with httpx.AsyncClient() as client:
            tasks = [client.get(test_url) for _ in range(num_requests)]
            await asyncio.gather(*tasks)

    async_times = []
    for _ in range(5):
        start = time.time()
        asyncio.run(run_async())
        async_times.append(time.time() - start)

    # 输出结果
    print(f"同步requests平均耗时: {statistics.mean(sync_times):.2f}s")
    print(f"多线程requests平均耗时: {statistics.mean(thread_times):.2f}s")
    print(f"异步httpx平均耗时: {statistics.mean(async_times):.2f}s")

    # 绘制图表
    plt.bar(['同步requests', '多线程requests(10)', '异步httpx'], 
            [statistics.mean(sync_times), statistics.mean(thread_times), statistics.mean(async_times)])
    plt.ylabel('完成100个请求的总时间(s)')
    plt.title('HTTP客户端性能对比')
    plt.show()

asyncio.run(benchmark())

这个测试会清晰地展示三种方式在处理100个请求时的性能差异,通常结果会是:异步httpx > 多线程requests > 同步requests。

何时选择httpx,何时坚持requests?

虽然httpx性能优越,但requests仍有其适用场景:

  1. 简单脚本或一次性任务:requests的简洁API难以超越
  2. 依赖同步环境的项目:如某些WSGI服务器
  3. 已有大量基于requests的代码库:迁移成本可能超过收益

而以下场景强烈推荐使用httpx:

  1. 高并发API调用:如微服务架构中的服务间通信
  2. 需要处理大量HTTP请求的数据采集任务
  3. 现代异步框架(如FastAPI)中的HTTP客户端需求
  4. 需要HTTP/2支持的应用

最后:性能与可维护性的平衡

httpx的性能优势源于其异步设计,但这并不意味着requests已经过时。选择工具时,除了性能还要考虑团队熟悉度、项目架构和长期维护成本。对于新项目,特别是基于异步框架的项目,httpx无疑是更现代、更高效的选择;而对于维护旧项目或简单脚本,requests的简洁性仍有其价值。

最后,无论选择哪个库,理解其底层原理和正确使用方式才是提升性能的关键。希望本文能帮助你做出更明智的技术选型决策。