剖析灵魂,为什么aiohttp默认的写法那么慢?

简介: 剖析灵魂,为什么aiohttp默认的写法那么慢?

摄影:产品经理与产品经理的环球旅行

在上一篇文章中,我们提到了 aiohttp 官方文档中的默认写法速度与 requests 单线程请求没有什么区别,需要通过使用asyncio.wait来加速 aiohttp 的请求。今天我们来探讨一下这背后的原因。

我们使用一个可以通过 URL 设定返回延迟的网站来进行测试,网址为:http://httpbin.org/delay/5。当delay后面的数字为 5 时,表示请求这个网址以后,要等 5 秒才会收到返回;当delay后面的数字为 3 时,表示请求这个网址以后,要等 3 秒才会收到返回。大家可以在浏览器上面输入这个网址测试看看。

现在我们写一段简单的 aiohttp 代码来进行测试:

import asyncio
import aiohttp
import time
asyncdef main():
    asyncwith aiohttp.ClientSession() as session:
        start = time.time()
        await session.get('http://httpbin.org/delay/3')
        await session.get('http://httpbin.org/delay/5')
        end = time.time()
        print(f'总共耗时:{end - start}')
asyncio.run(main())  # Python 3.7或以上程序直接执行这一行即可运行
# Python 3.6或以下需要注释掉上面一行,并为下面两行解除注释
#loop = asyncio.get_event_loop()
#loop.run_until_complete(main())

注意,如果你的 Python 版本大于等于 3.7,那么你可以直接使用asyncio.run来运行一个协程,而不需要像昨天那样先创建一个事件循环再运行。

运行效果如下图所示:

可以看到,运行时间大于 8 秒钟,也就是说,这段代码,是先请求第一个 3 秒的网址,等它运行完成以后,再请求第二个 5 秒的网址,他们根本就没有并行!

按照我们之前的认识,协程在网络 IO 等待的时候,可以交出控制权,当 aiohttp 请求第一个 3 秒网址,等待返回的时候,应该就可以立刻请求第二个 5 秒的网址。在等待 5 秒网址返回的过程中,又去检查第一个 3 秒请求是否结束了。直到 3 秒请求已经返回了结果,再等待 5 秒的请求。

那为什么上面这段代码,并没有按这段逻辑来走?

这是因为,协程虽然可以充分利用网络 IO 的等待时间,但它并不会自动这么做。而是需要你把它加入到调度器里面。

能被 await的对象有 3 种:协程、Task 对象、future 对象。

当你await 协程对象时,它并没有被加入到调度器中,所以它依然是串行执行的。

但 Task 对象会被自动加入到调度器中,所以 Task 对象能够并发执行。

要创建一个 Task 对象非常简单:

asyncio.create_task(协程) #python 3.7或以上版本的写法
asyncio.ensure_future(协程)  # python 3.6或以下的写法

所以我们来稍稍修改一下代码:

import asyncio
import aiohttp
import time
asyncdef main():
    asyncwith aiohttp.ClientSession() as session:
        start = time.time()
        task1 = asyncio.create_task(session.get('http://httpbin.org/delay/3'))
        task2 = asyncio.create_task(session.get('http://httpbin.org/delay/5'))
        await task1
        await task2
        end = time.time()
        print(f'总共耗时:{end - start}')
asyncio.run(main())  # Python 3.7或以上程序直接执行这一行即可运行

运行效果如下图所示:


可以看到,现在请求两个网址的时间加到一起,只比 5 秒多一点,说明确实已经实现了并发请求的效果。至于这多出来的一点点时间,是因为协程之间切换控制权导致的。

那么为什么我们把很多协程放进一个 列表里面,然后把列表放进 asyncio.wait里面,也能实现并行呢?这是因为,asyncio.wait帮我们做了创建 Task 的任务。这一点我们可以在 Python 的官方文档[1]中看到原话:

同理,当你把协程传入asyncio.gather时,这些协程也会被当做 Task 来调度:

回到我们昨天的问题,我们不用asyncio.wait也不用asyncio.Queue让爬虫并发起来:

import asyncio
import aiohttp
template = 'http://exercise.kingname.info/exercise_middleware_ip/{page}'
asyncdef get(session, page):
    url = template.format(page=page)
    resp = await session.get(url)
    print(await resp.text(encoding='utf-8'))
asyncdef main():
    asyncwith aiohttp.ClientSession() as session:
        tasks = []
        for page in range(1000):
            task = asyncio.create_task(get(session, page))
            tasks.append(task)
        for task in tasks:
            await task
asyncio.run(main())

运行效果如下图所示:


但你需要注意一点,创建 Task 与await Task是分开执行的:

tasks = []
for page in range(1000):
    task = asyncio.create_task(get(session, page))
    tasks.append(task)
for task in tasks:
    await task

你不能写成下面这样:

for task in range(1000):
    task = asyncio.create_task(get(session, page))
    await task

这是因为,创建 Task 的时候会自动把它加入到调度队列里面,然后await Task的时候执行调度。上面这样写,会导致每一个 Task 被分批调度,一个 Task 在等待网络 IO 的时候,没有办法切换到第二个 Task,所以最终又会降级成串行请求。

目录
相关文章
|
4月前
|
Java Spring 容器
什么情况下会导致@Async异步方法会失效?
什么情况下会导致@Async异步方法会失效?
|
5月前
|
监控 Serverless 文件存储
函数计算操作报错合集之启动服务时候超时,该如何解决
在使用函数计算服务(如阿里云函数计算)时,用户可能会遇到多种错误场景。以下是一些常见的操作报错及其可能的原因和解决方法,包括但不限于:1. 函数部署失败、2. 函数执行超时、3. 资源不足错误、4. 权限与访问错误、5. 依赖问题、6. 网络配置错误、7. 触发器配置错误、8. 日志与监控问题。
|
6月前
|
小程序 安全 算法
mPaaS问题之使用小程序传参数报错如何解决
mPaaS小程序是阿里巴巴移动平台服务(mPaaS)推出的一种轻量级应用解决方案,旨在帮助开发者快速构建跨平台的小程序应用;本合集将聚焦mPaaS小程序的开发流程、技术架构和最佳实践,以及如何解决开发中遇到的问题,从而助力开发者高效打造和维护小程序应用。
|
6月前
|
C#
C#学习系列相关之多线程(四)----async和await的用法
C#学习系列相关之多线程(四)----async和await的用法
|
6月前
|
IDE 测试技术 开发工具
FastAPI 并发请求解析:提高性能的重要特性
在当今的数字化世界中,网络用户对于高速响应和持续连接的诉求日益显著。这促使了基于 Python 构建的 FastAPI 框架受到广泛关注,它不仅现代化且效率极高,而且简化了并行请求的处理。本篇文章旨在探讨 FastAPI 如何处理这类请求,并对应用实例进行实际编码展示。
|
6月前
|
前端开发 JavaScript
setTimeout 函数在前端延迟搜索实现中的作用
setTimeout 函数在前端延迟搜索实现中的作用
|
前端开发 JavaScript
【前端用法】前端JS获取视频时长的写法
【前端用法】前端JS获取视频时长的写法
181 0
|
JSON 数据处理 数据格式
🌮微卷不亏,4 分钟优化 Fetch 函数写法~
上一篇介绍了啥叫“微卷不亏”,今天继续简单微卷一些小知识点:本篇带来《如何优化 Fetch 函数写法》,轻松拿下~
|
前端开发 JavaScript API
停止像这样使用 “async/await“,改用原版
停止像这样使用 “async/await“,改用原版
145 0
停止像这样使用 “async/await“,改用原版
|
缓存 JavaScript 开发者
require 函数加载模块原理(被加载的模块会先执行一次)|学习笔记
快速学习 require 函数加载模块原理(被加载的模块会先执行一次)
460 0
require 函数加载模块原理(被加载的模块会先执行一次)|学习笔记