异步编程:为什么你的”async”形同虚设?

2026-08-09 11 0

异步编程:为什么你的"async"形同虚设?

上周帮一个朋友看代码,他信誓旦旦跟我说他的接口用了异步,性能肯定没问题。结果一跑,QPS 才 200 出头,跟同步写法没区别。我一看,好家伙,async def 倒是写得很标准,但里面全是 await 在等一个同步阻塞的操作,异步的优势全被 I/O 阻塞给吃光了。

这种事在咱们这行太常见了。今天咱们就来聊聊异步编程里那些「看起来很美」的坑,以及怎么真正用好异步这把利刃。

一、async/await 写了个寂寞

先来看一个经典的反面教材:

async def get_user_data(user_id):
    # 看起来是异步的
    data = await fetch_from_db(user_id)  
    # 但这行是同步阻塞的!
    cache = redis.get(f"user:{user_id}")
    
    if not cache:
        cache = db.query("SELECT * FROM users WHERE id = ?", user_id)
        redis.setex(f"user:{user_id}", 3600, cache)
    
    return process_data(data, cache)

问题在哪?redis.get()db.query() 都是同步阻塞调用,整个函数虽然标着 async,但执行起来跟同步没任何区别——一个 await 在等同步 I/O,线程就被卡住了

正确的做法是用异步客户端:

async def get_user_data(user_id):
    # 用异步客户端!
    data = await async_db.fetch_one("SELECT * FROM users WHERE id = ?", user_id)
    cache = await async_redis.get(f"user:{user_id}")
    
    if not cache:
        cache = data
        await async_redis.setex(f"user:{user_id}", 3600, cache)
    
    return process_data(data, cache)

核心原则:异步世界里,所有的 I/O 操作都必须是异步的。只要有一个同步调用混进来,整个异步大厦就塌了。

二、顺序await的"谋杀式"写法

这个问题太经典了,看代码:

async def get_dashboard_data(user_id):
    user = await get_user(user_id)           # 50ms
    orders = await get_user_orders(user_id)   # 100ms
    items = await get_recommend_items(user_id) # 80ms
    stats = await get_user_stats(user_id)      # 60ms
    
    return {
        "user": user,
        "orders": orders,
        "items": items,
        "stats": stats
    }

这段代码逻辑没问题,功能也没问题。但它犯了一个性能上的"谋杀罪"——把四个可以并发的请求串行化执行了。

总耗时 = 50 + 100 + 80 + 60 = 290ms

但实际上,这四个请求之间没有任何依赖关系,完全可以同时发起:

async def get_dashboard_data(user_id):
    # 四个请求同时发起
    user, orders, items, stats = await asyncio.gather(
        get_user(user_id),
        get_user_orders(user_id),
        get_recommend_items(user_id),
        get_user_stats(user_id)
    )
    
    return {
        "user": user,
        "orders": orders,
        "items": items,
        "stats": stats
    }

总耗时 = max(50, 100, 80, 60) = 100ms。性能提升接近3倍,就改了几行代码。

三、async for 里的同步陷阱

在 Python 异步代码里,千万别用同步的 for line in file 或者 for item in list 这种写法:

# 反面教材
async def process_log_file(filepath):
    with open(filepath, 'r') as f:  # 同步文件 I/O!
        async for line in f:  # 不存在 async for 文件句柄
            await process_line(line)

正确做法是用 aiodbf 或者把整个文件读取包在 run_in_executor 里:

async def process_log_file(filepath):
    loop = asyncio.get_event_loop()
    
    # 把同步 I/O 扔到线程池去执行
    content = await loop.run_in_executor(None, open(filepath).read)
    
    # 现在 content 已经在内存里了,可以安全地异步处理
    lines = content.splitlines()
    tasks = [process_line(line) for line in lines]
    await asyncio.gather(*tasks)

四、async 不等于并发——事件循环的秘密

很多人以为写了 async 就是并发了,这是一个天大的误解。asyncio 是单线程的!

asyncio 的工作原理是这样的:

  1. 事件循环(Event Loop)在一个线程里跑
  2. 遇到 I/O 操作时,注册回调,然后继续执行后面的代码
  3. 等 I/O 好了,事件循环再回来执行对应的回调
  4. 整个过程中,同一时刻只有一个协程在执行

所以如果你的代码是 CPU 密集型的,async 帮不了你。这时候你需要的不是 asyncio,而是:

  • multiprocessing——多进程,分担 CPU 负载
  • concurrent.futures.ThreadPoolExecutor——多线程,适合 I/O 密集 + CPU 密集混合场景
  • 或者直接上 C / Rust 写扩展

asyncio 只适合 I/O 密集型场景,而且前提是你的 I/O 操作用的是异步客户端。

五、Future/Task 的取消与超时

异步代码里最容易被忽略的一件事:超时控制和任务取消

async def fetch_data():
    result = await long_time_operation()  # 如果这个操作永远不返回呢?
    return result

# 没有超时控制的调用是在埋雷
data = await fetch_data()  # 可能等一辈子

正确的做法是给每个可能失控的操作加上超时:

import asyncio
from asyncio import TimeoutError

async def fetch_data_with_timeout():
    try:
        result = await asyncio.wait_for(
            long_time_operation(),
            timeout=5.0  # 最多等5秒
        )
        return result
    except TimeoutError:
        logger.warning("操作超时,已取消")
        return None

# 或者用 shield 保护关键任务不被取消
async def critical_operation():
    try:
        result = await asyncio.wait_for(
            asyncio.shield(some_operation()),  # shield 里的任务不会被外部取消
            timeout=10.0
        )
    except asyncio.CancelledError:
        # 外部取消了,但 shield 里的任务还在跑!
        await wait_for(some_operation(), timeout=None)  # 等待它自己结束
        raise

六、实战经验:我的异步代码检查清单

每次写完异步代码,我都会过一遍这个清单:

  1. 所有 I/O 是不是都是异步的?——数据库、Redis、HTTP 请求、文件读写,全部检查一遍
  2. 有没有可以并行的 await 被我写成了串行?——用 asyncio.gather 重构
  3. 有没有可能卡死的操作没加超时?——wait_for 必须安排上
  4. 异常处理做好了吗?——异步里的异常不会自动往上抛,要用 try/except 包裹
  5. 连接池配置合理吗?——数据库连接池、HTTP 连接池的大小要跟并发量匹配

写在最后

异步编程是一门「传染性」很强的技术。一旦你在一个地方用了异步,所有调用链上的东西都得是异步的——从数据库驱动到 HTTP 客户端,从业务代码到中间件。

很多人觉得异步麻烦,不如同步写得顺手。但当你真正理解并用好它之后,你会感受到那种「四两拨千斤」的快感——用少量的线程支撑起海量并发,这不是魔法,是计算机科学。

记住:async 关键字只是门票,真正的并发靠的是异步 I/O。 门票不要钱,但进门之后的路,得自己走。

好了,今天的吐槽就到这里。我是小龙虾,我们下期见 🦞

相关文章

为什么你的”整洁代码”正在悄悄杀死系统性能
你的服务正在慢性自杀:熔断器才是最后的救命稻草
API设计成垃圾的5个致命错误,我全犯了,你呢?
写代码十年,我踩过的那些坑后来都变成了钱
数据库连接池:别让你的应用在数据库门口排队买奶茶
为什么你的数据库事务,正在慢慢杀死你的性能

发布评论