100个HTTP请求要多久?同步300秒,异步3秒——Python异步实战

同步代码在等IO时CPU空转,异步代码在等IO时去干别的事——这就是两者性能差距的根本原因。本文从实战出发,讲清楚asyncio的核心概念、常见坑、以及真实应用场景,让你从"懂语法"升级到"能实战"。

一个请求3秒,100个请求要多久?

同步做法:300秒。异步做法:可能是3秒。

差距不在代码量,在认知。

很多人学不会asyncio,不是因为它难,是因为从一开始理解错了方向。本文不讲语法讲语法,从实战出发,告诉你什么时候该用async,怎么用对async,以及async最常见的坑。

一、为什么你的代码需要异步

1.1 同步vs异步:一张图讲清楚

同步代码的核心特征:

# 同步代码
result = requests.get("https://api.example.com/data")  # 程序在这里停住,等服务器回复
print(result)  # 收到回复后,继续

程序在requests.get()这一行完全停住。CPU在等网络IO返回的这段时间里,什么都没干。

异步代码的核心特征:等待期间不闲着

# 异步代码
async def fetch():
    result = await requests.get("https://api.example.com/data")  # 发起请求,然后去干别的事
    return result

await不是”等”,是”去干别的事,等那边好了再回来”。

关键理解:异步不是让单个请求变快,是让等待的时间被利用起来。

1.2 什么时候该用async

异步有代价:代码更复杂,调试更困难。

该用async的场景

  • 网络请求(HTTP、数据库、Redis等IO操作)
  • 文件读写(大文件尤其实用)
  • 同时处理多个连接(Web服务器、爬虫)
  • 等待多个外部资源(API调用、并发查询)

不该用async的场景

  • CPU密集型任务(图片处理、视频压缩、加密计算)—— async帮不上忙,用multiprocessing
  • 单个简单请求—— overhead不值得
  • 不理解原理的情况下”为了异步而异步”

二、asyncio核心概念

2.1 协程(Coroutine):异步的基本单位

协程是asyncio的最小单位。本质是一个可以暂停和恢复的函数。

import asyncio

# 定义一个协程
async def hello():
    print("开始")
    await asyncio.sleep(1)  # 暂停,去做别的事
    print("结束")

# 运行协程
asyncio.run(hello())

注意:async def定义的函数是协程函数,调用它不会直接执行,会返回一个协程对象。

2.2 事件循环(Event Loop):异步的大脑

事件循环是asyncio的核心引擎。它的职责是:

  1. 调度协程的执行顺序
  2. 在IO等待时切换任务
  3. 协调协程之间的切换
import asyncio

# 获取当前事件循环
loop = asyncio.get_event_loop()

# 手动运行事件循环
loop.run_until_complete(hello())

Python 3.7+简化了语法:

asyncio.run(hello())  # 自动创建事件循环并运行

2.3 Task与Future:让协程真正跑起来

协程只是”可以暂停”的对象,真正让它跑起来需要Task。

async def main():
    task = asyncio.create_task(hello())  # 创建Task,立即开始调度
    print("我先执行")
    await task  # 等待hello完成

asyncio.run(main())

create_task会让协程进入事件循环,立即开始执行(而不是等到await才执行)。

多个任务并发:

async def main():
    # 创建3个并发任务
    tasks = [
        asyncio.create_task(hello()),
        asyncio.create_task(hello()),
        asyncio.create_task(hello()),
    ]
    await asyncio.gather(*tasks)  # 等待所有任务完成

asyncio.run(main())
# 耗时:约1秒(3个任务并行),而不是3秒

三、实战踩坑指南

3.1 常见错误:忘了await

# 错误写法
async def fetch_all():
    results = [fetch(url) for url in urls]  # 没有await,返回的是协程对象列表,不是结果
    return results

# 正确写法
async def fetch_all():
    results = await asyncio.gather(*[fetch(url) for url in urls])  # gather自动await每个协程
    return results

记住:协程函数不加await调用,返回的是协程对象,不是结果。

3.2 常见错误:在同步函数里调async

# 错误写法
def sync_function():
    result = asyncio.run(async_function())  # 可以运行,但每次都创建新的事件循环,效率极低

# 正确做法:如果是同步函数,就保持同步
async def async_caller():
    result = await async_function()
    return result

原则:异步代码一旦开始,尽量全链路异步。中途混用同步会失去异步优势,甚至更慢。

3.3 性能陷阱:async不自动等于快

# 假异步(串行执行)
async def false_async():
    for url in urls:
        await fetch(url)  # 每次await等待完成才执行下一个,跟同步没区别

# 真异步(并发执行)
async def true_async():
    tasks = [fetch(url) for url in urls]
    await asyncio.gather(*tasks)  # 一次性发起所有请求,并发等待

判断标准:看代码里有多少个await,以及它们是在循环里还是循环外。

四、经典应用场景

4.1 并发HTTP请求

这是async最经典的应用场景。用aiohttp而不是requests:

import asyncio
import aiohttp

async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        return await asyncio.gather(*tasks)

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

# 100个URL,总耗时约等于最慢的一个,而不是100个之和

4.2 数据库操作

异步数据库用asyncpg(PostgreSQL)或aiomysql(MySQL):

import asyncio
import asyncpg

async def query_many():
    conn = await asyncpg.connect(host="localhost", database="mydb", user="user", password="pwd")
    
    queries = ["SELECT * FROM users WHERE id = $1" for id in range(1, 101)]
    
    # 并发执行100个查询
    results = await asyncio.gather(*[
        conn.fetch(queries[i % len(queries)]) for i in range(100)
    ])
    
    await conn.close()
    return results

五、总结与行动指南

asyncio的核心就三句话:

  1. 协程是基本单位,用async def定义
  2. await是切出切回,不是等待
  3. 真正的并发靠asyncio.gather或create_task

记住三个不要:

  • 不要在循环里await然后以为自己在做并发
  • 不要在同步代码里混用asyncio.run()
  • 不要对CPU密集型任务用async(没有IO等待,异步没有意义)

下一步行动:
把项目里用requests做的批量网络请求改成aiohttp,这是最容易上手、收益最明显的第一步。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注