写了个爬虫,抓一百个网页,同步跑要三分钟,其中两分半在等网络响应。CPU闲得发慌,你却在干等。这不是代码写得慢,是模式选错了。同步编程像排队打饭,一个人打完才轮到下一个;异步编程像同时开十个窗口,谁好了谁先端走。
async/await不是语法糖,是思维转弯
刚接触异步的人,容易把async def当成普通函数加个装饰。一调用,返回个协程对象,不执行,懵了。协程不是函数,是“可以暂停和恢复的任务”。await不是等待,是“把控制权交出去,等结果好了再回来”。理解这一点,才算摸到门。同步是你等我,异步是我先忙别的,你好了叫我。一个await,背后是事件循环在调度成百上千个任务。
事件循环是异步的心脏
没有事件循环,协程就是死物。asyncio.run()启动循环,它维护一个任务队列,谁准备好了就执行谁。遇到await,当前任务挂起,循环去跑别的。网络IO、文件读写、数据库查询,这些等待时间全被循环拿去干别的活。事件循环像一个精明的调度员,绝不让CPU闲着。你写的是单线程代码,跑起来却像多线程在干活,这就是异步的魔力。
实战:aiohttp并发抓取
用requests写爬虫,一百个URL循环抓,每个等一秒,总共一百秒。换成aiohttp加asyncio.gather,一百个请求同时发,总耗时取决于最慢的那个,可能两秒搞定。核心代码就几行:定义异步函数,用async with创建会话,await session.get()发请求,最后gather打包所有任务。从一百秒到两秒,不是优化,是换了个活法。但注意,gather不是万能,任务太多会打爆对方服务器,加个Semaphore控制并发数。
避坑:异步不是银弹
有人把整个项目改成异步,结果数据库驱动不支持,CPU密集任务照样卡死事件循环。异步适合IO密集,不适合计算密集。CPU密集老老实实上多进程。还有,异步代码里混用同步阻塞调用,比如time.sleep或者requests,会卡死整个循环。一个同步调用混进异步,就像高速路上突然有人停车,后面全堵死。用asyncio.to_thread把阻塞调用扔到线程池,或者换成异步库。
调试和异常,别让协程悄悄死掉
异步任务抛异常,如果没await它,异常不会冒出来,任务默默死掉,你还在等结果。用asyncio.gather(..., return_exceptions=True)收集异常,或者给任务加add_done_callback。调试时开asyncio的debug模式,慢任务会告警。异步的坑大多不在语法,在“以为它跑了,其实它早挂了”。
异步编程不是炫技,是让IO等待的时间产生价值。从同步到异步,最难的不是写async和await,是脑子里那根“等”的弦换成“调度”的弦。代码还是那点代码,思维一换,效率翻倍。入门靠文档,实战靠踩坑,踩多了,自然就通了。