前言
前五篇文章介绍了 asyncio
的 API,从这篇开始,就要讲一些 Real World(并不)的东西了。
使用 aiohttp 作为 HTTP 客户端
aiohttp 是一个基于 asyncio
的异步 HTTP 客户端和服务器库,也是 asyncio
生态中发展最迅速的第三方库之一。在这一节,我们使用 aiohttp 作为 HTTP 客户端来比较一下同步、基于线程的异步和基于 asyncio
的异步的差别。
准备工作
首先我们安装好所需的第三方库:
1 | pip install requests |
准备一些用于并发请求的 url,共 45 个:
1 | # url.py |
同步的请求
首先导入所需的库:
1 | import time |
完成请求单个 url 的函数,这个函数会以 bytes
形式返回网站内容:
1 | def fetch(session, url): |
同步请求所有的 url,打印出字节的长度:
1 | def main(): |
记录完成请求所需的时间:
1 | if __name__ == '__main__': |
上述代码的结果:
1 | http://caipiao.hao123.com/: 109299 |
可以看出打印的顺序是和 url 列表的顺序完全一致的,同步的代码耗时约 24s。
基于线程的请求
首先导入所需的库:
1 | import time |
完成单个请求的函数:
1 | def fetch(session, url): |
使用线程池请求所有的 url:
1 | def main(): |
记录完成所需的时间:
1 | if __name__ == '__main__': |
上述代码的结果:
1 | http://www.ccb.com/: 276 |
可以看到返回结果的顺序并不和 url 列表一致,准确的说,是按照请求完成的顺序排列的。同时,请求所需的时间大幅缩短,降到了约 9s。
基于 asyncio
的请求
首先导入所需的库:
1 | import asyncio |
完成单个请求的函数:
1 | async def fetch(session, url): |
这里同时返回了请求的 url 和网站内容,是因为后面的代码不容易在请求完成后获得请求的 url。
使用 aiohttp
请求所有的 url:
1 | async def main(): |
开启事件循环,并记录所需的时间:
1 | if __name__ == '__main__': |
上述代码的结果:
1 | http://www.psbc.com/: 404 |
和使用线程一样,返回结果是按照请求完成顺序排列的。请求的时间比线程更短,只用了约 2s 就完成了所有的请求。和使用线程的方式相比,asyncio
避免了创建线程的开销。
保存请求的结果
需要注意的是,上述请求只是简单的获取了内容,这些 bytes 只在内存中存在。一旦我们需要把结果保存到磁盘,就会有另一个会导致异步代码退化到同步的地方:磁盘 I / O。
现在我们增加一个保存请求内容到磁盘的函数:
1 | from urllib.parse import quote_plus |
同时增加一个函数,用来同时发起请求并把结果保存到文件:
1 | async def fetch_and_save(session, url): |
同时更新一下 main()
函数:
1 | async def main(): |
上述代码的结果:
1 | save: http://www.psbc.com/ |
可以看到消耗的时间增加到了约 10s。
有没有什么方法可以将同步的文件系统操作变为异步的呢?答案就是结合使用线程和 asyncio
。修改一下 fetch_and_save()
函数,使其在其他线程中执行保存操作:
1 | async def fetch_and_save(session, url): |
修改后的结果:
1 | save: http://www.psbc.com/ |
效果很明显,所需的时间缩短到了约 5s。
NOTE:需要注意的是,大多数操作系统上并未提供文件系统的异步 I / O 操作(Linux kernel 提供了文件系统异步 I / O,不过它需要一个额外的库 aio),大部分的异步框架都是使用线程处理文件系统 I / O 的。如果需要统一的 API,可以选择 aiofiles。