Python 异步编程常见误区总结

12次阅读

async/await 不等于自动并发,单个 await 是顺序等待,需用 asyncio.gather() 或 create_task() 显式并发;CPU 密集型任务应使用 ProcessPoolExecutor;未 await 可等待对象会导致逻辑跳过或静默失败;异步上下文须用 contextvars 而非 Threading.local。

Python 异步编程常见误区总结

async/await 不等于自动并发

很多人以为只要把函数改成 async def,再用 await 调用,程序就会“变快”或“自动并行”。事实并非如此:单个 await 是顺序等待,不触发并发;只有显式调度多个协程(如用 asyncio.gather()asyncio.create_task())才可能并发执行。

常见错误现象:
– 写了 5 个 await fetch_data() 连续调用,耗时接近 5 倍单次;
– 混淆 await asyncio.sleep(1)time.sleep(1),后者会阻塞整个事件循环

  • 真正并发需用 asyncio.gather(task1(), task2(), task3()) 批量 await
  • asyncio.create_task() 提前启动协程,适合有依赖或需控制生命周期的场景
  • CPU 密集型任务不能靠 async 加速,应改用 concurrent.futures.ProcessPoolExecutor

在同步代码里直接调用 async 函数会报错

比如在普通函数里写 result = my_async_func(),实际返回的是 coroutine 对象,不是结果;若不 await 或用 asyncio.run() 驱动,就只是个未执行的协程——后续一旦尝试打印或使用,大概率触发 RuntimeWarning: coroutine 'xxx' was never awaited

使用场景常见于:单元测试、脚本快速验证、与老代码混用。

立即学习Python免费学习笔记(深入)”;

  • 调试时想临时跑一个 async 函数?用 asyncio.run(my_async_func())
  • 在同步上下文中需要等结果?必须用 asyncio.run(),不能只调用不驱动
  • 切勿在 __init__@Property、日志格式化等同步钩子里直接调 async 函数

忘记 await 可等待对象(如 Task、Future)导致逻辑跳过

asyncio.create_task() 返回的是 Task 对象,它本身是可等待的(awaitable),但如果不 await 它,协程就只是被调度、然后被丢弃——任务可能中途被取消,也可能静默失败,而主协程早已结束。

典型错误:
task = asyncio.create_task(fetch_user()) 后没 await task
– 用 asyncio.ensure_future() 但没收集返回值或 await;
– 在 try/except 外层漏掉 await,导致异常无法被捕获。

  • 所有通过 create_task()ensure_future()to_thread() 启动的可等待对象,都应明确 await 或加入 gather
  • 若需“发完即忘”,至少加 asyncio.current_task().get_loop().create_task(...) 并确保 loop 不提前关闭
  • asyncio.wait_for(task, timeout=...) 包一层,避免无限挂起

误用 threading.local 在异步上下文中失效

很多开发者习惯用 threading.local() 存储请求上下文(如用户 ID、trace_id),但在 asyncio 中,协程可能在不同线程间切换(尤其用了 loop.run_in_executor),且单线程内多个协程共享同一 thread-local,导致数据污染或丢失。

表现症状:
– A 请求的 trace_id 意外出现在 B 请求日志中;
local.var = 'x' 后,在另一个 await 点取不到值;
– 使用 contextvars 前,用 threading.local 实现的中间件在压测下出错。

  • 异步上下文隔离请用 contextvars.ContextVar,它是 asyncio 原生支持的
  • ContextVar 必须在协程开始时 set(如中间件入口),并在每个 await 分界点后仍有效
  • 不要试图在 run_in_executor 的子线程里读写主线程ContextVar,需手动传递值

async/await 的边界比看起来更硬:它不是语法糖,而是一套协作式调度契约。最常被忽略的,是「可等待对象」必须被显式驱动,以及 context 隔离机制完全不同于线程模型——这两点一旦出错,问题往往延迟暴露、难以复现。

text=ZqhQzanResources