## 关于Python的asyncio一次说清楚最近几年在Python社区里asyncio这个词出现的频率越来越高。很多项目开始依赖它招聘要求里也常看到“熟悉asyncio”的字样。但说实话这东西刚出来的时候不少老Python开发者都觉得有点懵——我们不是已经有线程和进程了吗这个asyncio到底是个什么为什么需要它它是什么简单来说asyncio是Python标准库里的一个模块用来写并发代码。注意这里说的是“并发”而不是“并行”这是两个不同的概念。打个比方你一个人同时照看三个炉子做饭这就是并发——你在几个任务之间快速切换每个炉子都在慢慢加热但你并没有分身术。而并行更像是你找了两个帮手三个人同时操作三个炉子。asyncio实现并发的方式很特别它基于“事件循环”和“协程”。事件循环就像是个总调度员协程则是一个个可以暂停和恢复的任务。当某个协程需要等待比如等网络数据返回它就会主动说“我先让出位置等有结果了再叫我”然后事件循环就去处理其他就绪的协程。这种方式最大的好处是在等待I/O比如网络请求、文件读写的时候CPU不用干等着可以去干别的活。这和我们熟悉的线程很不一样。线程是由操作系统调度的切换线程需要保存和恢复很多状态开销比较大。而协程的切换完全在用户态完成轻量得多。一个线程里可以跑成千上万个协程但开成千上万个线程系统可能就撑不住了。它能做什么asyncio最适合的场景是I/O密集型的应用。什么是I/O密集型就是程序大部分时间都在等待外部操作完成比如等数据库返回结果、等另一个服务接口响应、等文件从磁盘读取出来。在这些等待的时间里如果用传统同步的方式CPU就闲着如果用多线程线程切换又有开销。现在很多Web后端服务就是典型的I/O密集型场景。一个用户请求过来可能需要查三次数据库、调用两个外部API。如果每个请求都卡在那里等服务器能同时处理的请求数就很有限。用asyncio的话在等待数据库响应的间隙CPU可以去处理其他用户的请求服务器的吞吐量就能大幅提升。但要注意asyncio不适合CPU密集型的任务。如果你的代码大部分时间都在做数学计算、图像处理这类纯CPU运算用asyncio反而可能更慢。因为协程的切换也需要成本而且Python有全局解释器锁GIL一个时刻只有一个线程在执行Python字节码。对于CPU密集型任务多进程通常是更好的选择。怎么使用用asyncio写代码和写同步代码的感觉不太一样。最明显的区别是你需要用async和await这两个关键字。定义一个协程函数要在def前面加上asyncasyncdeffetch_data(url):# 模拟一个耗时的网络请求awaitasyncio.sleep(2)returnfData from{url}调用协程函数时要在前面加上awaitasyncdefmain():resultawaitfetch_data(https://example.com)print(result)这个await就是“等待”的意思但它不是傻等。当执行到await时当前协程会暂停把控制权交还给事件循环事件循环就可以去执行其他协程。等到fetch_data完成了事件循环会再回来继续执行main函数。要运行这个协程需要把它交给事件循环asyncio.run(main())实际项目中我们经常需要同时处理多个任务。比如要抓取10个网页如果一个个顺序抓取要等很久如果同时发起10个请求等最慢的那个返回就行了。asyncio提供了很多工具来处理这种情况asyncdefmain():urls[url1,url2,url3]tasks[fetch_data(url)forurlinurls]resultsawaitasyncio.gather(*tasks)# 所有结果都返回了asyncio.gather会并发运行所有任务等所有任务都完成后再继续。这样三个网络请求几乎是同时发出去的总时间差不多等于最慢的那个请求的时间而不是三个请求时间的总和。最佳实践用asyncio写代码有些地方需要特别注意。首先是不要阻塞事件循环。事件循环是单线程的如果你在协程里执行了一个很耗时的同步操作比如复杂的计算、同步的网络请求整个事件循环就会被卡住所有其他协程都动不了。怎么避免对于CPU密集型操作可以考虑放到线程池里执行importasyncioimporttimedefheavy_calculation():time.sleep(5)# 模拟耗时计算return42asyncdefmain():loopasyncio.get_running_loop()# 把耗时函数放到线程池里执行resultawaitloop.run_in_executor(None,heavy_calculation)其次是异常处理。在同步代码里异常会沿着调用栈向上传递。但在asyncio里如果在一个任务里发生了异常而这个异常没有被捕获默认情况下这个任务就静默失败了不会影响其他任务。这有好有坏好处是一个任务崩溃不会拖垮整个程序坏处是可能隐藏了错误。好的做法是给重要的任务加上异常处理或者用asyncio.create_task创建任务时保留任务对象后面检查是否有异常。还有一个常见的问题是在同步代码里调用异步代码。这是不允许的会报错。如果你的项目是从同步代码逐步迁移到异步的可能会遇到这种问题。通常的解决方案是把调用异步代码的地方也改成异步的或者用asyncio.run在同步代码里启动一个全新的事件循环——但后者要小心不要在已经运行事件循环的线程里再开一个。最后是选择合适的库。不是所有的Python库都支持asyncio。如果一个库是同步的在协程里调用它就会阻塞事件循环。现在很多常用的库都有了异步版本比如aiohttp对应requestsaiomysql对应mysql-connector。在选型时要留意这一点。和同类技术对比经常有人问有了asyncio还要线程和进程吗答案是都要它们解决的是不同的问题。多进程适合CPU密集型任务特别是那些需要利用多核CPU的计算。每个进程有自己独立的内存空间数据不共享避免了复杂的同步问题但进程间通信成本较高。Python的multiprocessing模块就是干这个的。多线程适合I/O密集型任务而且代码写起来比较直观。但Python的多线程有个硬伤GIL。因为GIL的存在多线程并不能真正并行执行Python字节码。对于纯Python的CPU密集型任务多线程可能比单线程还慢因为线程切换有开销。但对于I/O密集型任务线程在等待I/O时会释放GIL所以还是能提高并发能力的。asyncio也适合I/O密集型任务而且比线程更轻量、更高效。一个线程里可以跑成千上万个协程但开成千上万个线程系统可能就崩溃了。asyncio的缺点是学习曲线较陡需要改变编程思维而且生态系统还在发展中不是所有库都支持。那么到底选哪个呢有个简单的判断方法如果你的程序大部分时间在等I/O用asyncio如果大部分时间在做计算用多进程如果两种都有可以考虑混合使用——用asyncio处理I/O把计算密集型部分丢到线程池或进程池里。实际上很多现代框架都在走混合路线。比如FastAPI底层用asyncio处理网络I/O但如果你有个函数是CPU密集型的它可以自动帮你放到线程池里执行既保持了异步的高并发又避免了阻塞事件循环。最后想说技术选型没有银弹。asyncio很强大但也不是万能的。理解每种技术的适用场景和限制根据实际需求做选择这才是专业开发者的做法。有时候简单的同步代码可能比复杂的异步方案更合适特别是当团队对asyncio不熟悉或者项目规模不大的时候。代码毕竟是写给人看的维护成本也是需要考虑的重要因素。