一篇文章理解Python异步编程的基本原理

摄影:产品经理

与产品经理一起屯积一周的口粮

未闻 Code 已经发布过很多篇关于异步爬虫与异步编程的文章,最近有读者希望我能深入介绍一下 asyncio 是如何通过单线程单进程实现并发效果的。以及异步代码是不是能在所有方面都代替同步代码。

一些例子

第一个例子

假设你需要用电饭煲煮饭,用洗衣机洗衣服,给朋友打电话让他过来吃饭。其中,电饭煲需要30分钟才能把饭煮好,洗衣机需要40分钟才能把衣服洗好,朋友需要50分钟才能到你家。那么,是不是你需要在这三件事情上面消耗30 + 40 + 50 = 120分钟?

实际上,在现实中你只需要消耗50分钟就可以了——

先给朋友打电话,让他现在出门

把衣服放进洗衣机并打开电源

把米淘洗干净,放进电饭煲并打开电源

然后,你要做的就是等待。

第二个例子

现在,你需要完成语文试卷,数学试卷和英语试卷。每张试卷需要做1小时。于是你需要1 + 1 + 1 = 3小时来完成所有的试卷。没人帮你,所以你没有办法在少于3小时的情况下完成这三张试卷。

第三个例子

现在,你需要用电饭煲煮饭、用洗衣机洗衣服,并完成一张数学试卷。其中,电饭煲需要30分钟才能把饭煮好,洗衣机需要40分钟才能把衣服洗好,试卷需要1小时才能完成。

但你并不需要30 + 40 + 60 = 130分钟。你只需要70分钟左右——

1.把衣服放进洗衣机并打开电源

2.把米淘洗干净,放进电饭煲并打开电源

3.开始完成试卷

能异步与不能异步

在第一个例子里面,煮饭、洗衣、等朋友有一个共同点,就是每个操作看似耗时很长,但真正需要人去操作的只有很少的时间:淘米、打开电饭煲电源用时5分钟;把衣服放进洗衣机,打开电源用时2分钟;给朋友打电话用时1分钟。剩下的大部分时间都不需要人来操作,都是等待即可。

再看第二个例子,每一张试卷都会占用整个你,没有等待的时间,所以必须一张一张试卷完成。

这两个例子实际上对应了两种程序类型:I/O 密集型程序和计算密集型程序。

我们在使用 requests 请求 URL、查询远程数据库或者读写本地文件的时候,就是 I/O操作。这些操作的共同特点就是要等待。

以 request 请求URL 为例,requests 发起请求,也许只需要0.01秒的时间。然后程序就卡住,等待网站返回。请求数据通过网络传到网站服务器,网站服务器发起数据库查询请求,网站服务器返回数据,数据经过网线传回你的电脑。requests 收到数据以后继续后面的操作。

大量的时间浪费在等待网站返回数据。如果我们可以充分利用这个等待时间,就能发起更多的请求。而这就是异步请求为什么有用的原因。

但对于需要大量计算任务的代码来说,CPU 始终处于高速运转的状态,没有等待,所以就不存在利用等待时间做其它事情的说法。

所以:异步只适用于 I/O 操作相关的代码,不适用于非 I/O操作。

Python 的异步代码

上面我们使用生活中的例子来说明异步请求,这可能会给大家一种误解——我可以控制代码,让代码在我想让他异步的地方异步,不想异步的地方同步。例如,可能有人会希望能用下面这段伪代码所描述方式来写代码:

请求 https://baidu.com,在网站返回期间: a = 1 + 1 b = 2 + 2 c = 3 + 3

拿到返回的数据,做其他事情

就像是我们把电饭煲的电源插上后,等待饭煮好的过程中,我可以看书,可以打电话,可以看电视,想做什么就做什么。

这段伪代码写得很符合直觉,但在使用 Python里面不能这样写。

下面我们用一段真正的代码,来说明这样写有什么问题。

首先,我们做一个网站,当我们请求http://127.0.0.1:8000/sleep/时,网站会等待num秒才会返回。例如:http://127.0.0.1:8000/sleep/3表示,当你发起请求后,网站会等待3秒钟再返回。运行效果如下图所示。

现在,我们使用 aiohttp 发送3次请求,分别等待1秒、2秒、3秒返回:

import aiohttp

import asyncio

import time

async def request(sleep_time): async with aiohttp.ClientSession() as client: resp = await client.get(f'http://127.0.0.1:8000/sleep/{sleep_time}') resp_json = await resp.json() print(resp_json)

async def main(): start = time.perf_counter() await request(1) a = 1 + 1 b = 2 + 2 print('能不能在第一个请求等待的过程中运行到这里?') await request(2) print('能不能在第二个请求等待的过程中运行到这里?') await request(3) end = time.perf_counter() print(f'总计耗时:{end - start}')

asyncio.run(main())

运行效果如下图所示:

在图中第15行代码,发起了1秒的请求,那么第15行应该会等待1秒钟才会返回数据。而第16、17、18行都是简单的赋值和 print 函数,运行时间加在一起都显然小于1秒钟,所以理论上我们看到的返回应该是:

能不能在第一个请求等待的过程中运行到这里?

能不能在第二个请求等待的过程中运行到这里?

{'success': True, 'time': 1}

{'success': True, 'time': 2}

{'success': True, 'time': 3}

总计耗时:3.018130547

但实际上,我们看到的效果,却是:程序先运行到第15行,等待请求完成网站返回以后,再运行16,17,18行,然后运行19行,等2秒请求完成了,再运行第20行,最后运行第21行。3次请求串行发出,最终耗时6秒。

程序的运行逻辑与我们期望的不一样。程序并没有利用 I/O 等待的时间发起新的请求,而是等上一个请求结束了再发送下一个请求。

问题出在哪里?

问题出现在,Python 的异步代码,请求之间的切换不能由开发者来直接管理。

开发者通过await语句告诉 asyncio,它后面这个函数,可以被异步等待。注意是可以被等待,但要不要等待,这是 Python 底层自己来决定的。

因为一个 I/O 操作,无论你是发网络请求,还是读写硬盘,Python 都知道,所以当 Python 发现你现在的这个操作确实是一个 I/O操作时,它才会利用I/O 等待时间。

所以,在 Python 的异步编程中,开发者能做的事情,就是把所有能够异步的操作,一批一批告诉 Python。然后由 Python 自己来协调、调度这批任务,并充分利用等待时间。开发者没有权力直接决定这些 I/O操作的调度方式。

所以,上面的代码我们需要做一些修改:

import aiohttp

import asyncio

import time

async def request(sleep_time): async with aiohttp.ClientSession() as client: resp = await client.get(f'http://127.0.0.1:8000/sleep/{sleep_time}') resp_json = await resp.json() print(resp_json)

async def main(): start = time.perf_counter() tasks_list = [ asyncio.create_task(request(1)), asyncio.create_task(request(2)), asyncio.create_task(request(3)), ] await asyncio.gather(*tasks_list) end = time.perf_counter() print(f'总计耗时:{end - start}')

asyncio.run(main())

运行效果如下图所示:

可以看到,现在耗时3秒钟,说明这3次请求,确实利用了请求的等待时间。

我们通过asyncio.create_task()把不同的协程定义成异步任务,并把这些异步任务放入一个列表中,凑够一批任务以后,一次性提交给asyncio.gather()。于是,Python 就会自动调度这一批异步任务,充分利用他们的请求等待时间发起新的请求。

我们平时在写 Scrapy 爬虫时,会有类似下面这样的代码:

...

yield scrapy.Request(url, callback=self.parse)

next_url = url + '&page=2'

yield scrapy.Request(next_url, callback=self.parse)

看起来像是先“请求”url,然后利用这个请求的等待时间执行next_url = url + '&page=2'接下来再发起另一个请求。

但实际上,在 Scrapy 内部,当我们执行yield scrapy.Request后, 仅仅是把一个请求对象放入 Scrapy 的请求队列里面,然后就继续执行next_url = url + '&page=2'了。

请求对象放进请求队列后,还没有真正发起 HTTP请求。只有凑够了一定数量的请求对象或者等待一段时间以后,Scrapy 的下载器才会统一调度这一批请求对象,统一发送 HTTP请求。当某个请求返回以后,Scrapy 把返回的 HTML 组装成 Response 对象,并把这个对象传入 callback 函数执行后续操作。

综上所述,在 Python 里面的异步编程,你需要先凑够一批异步任务,然后统一提交给 asyncio,让它来帮你调度这批任务。你不能像 JavaScrapt 中那样手动直接控制在异步请求等待时执行什么代码。

在异步代码中调用同步函数

在异步函数里面是可以调用同步函数的。但是如果被调用的同步函数很耗时,那么就会卡住其他异步函数。例如print函数就是一个同步函数,但是由于它耗时极短,所以不会卡住异步任务。

我们现在写一个基于递归的斐波那契数列第 n 项计算函数,并在另一个异步函数中调用它:

def sync_calc_fib(n): if n in [1, 2]: return 1 return sync_calc_fib(n - 1) + sync_calc_fib(n - 2)

async def calc_fib(n): result = sync_calc_fib(n) print(f'第 {n} 项计算完成,结果是:{result}') return result

众所周知,基于递归的方式计算斐波那契数列第 n 项,速度非常慢,我们计算一下第36项,可以看到耗时在5秒钟左右:

如果我们把计算斐波那契数列(CPU 密集型)与请求网站(I/O密集型)任务放在一起会怎么样呢?

我们来看看效果:

可以看出,总共耗时8秒左右,其中计算斐波那契数列第36项耗时5秒,剩下3次网络请求耗时3秒,所以总共耗时8秒。

这段代码说明,当一个异步函数(calc_fib)中调用了一个耗时非常长的同步函数(sync_calc_fib)时,这一批所有的异步任务都会被卡住,只有这个同步函数运行完成以后,其他的异步函数才能被正常调度。这就是为什么在异步编程里面,不建议使用 time.sleep的原因。

kingname

攒钱给产品经理买房。

文章来源: blog.51cto.com,作者:未闻Code,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.51cto.com/15023263/2558778

python实现异步的原理_一篇文章理解Python异步编程的基本原理相关推荐

  1. python 的库如何开发_一篇文章入门Python生态系统

    译者按:原文写于2011年末,虽然文中关于Python 3的一些说法可以说已经不成立了,但是作为一篇面向从其他语言转型到Python的程序员来说,本文对Python的生态系统还是做了较为全面的介绍.文 ...

  2. c++ socket线程池原理_一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题...

    编辑:业余草来源:https://www.xttblog.com/?p=4946 一篇文章看懂 ThreadLocal 原理,内存泄露,缺点以及线程池复用的值传递问题. ThreadLocal 相信不 ...

  3. 一篇文章理解 同步异步、阻塞非阻塞

    前述 同步异步,阻塞非阻塞是一些非常常见的概念,但是对于开发者来说往往是用到了很难说清楚. 笔者专门整理了下这方面的概念,作此文以记之. 这部分内容可能存在一些争议,如有不同意见欢迎评论交流. 概念 ...

  4. 有关python方面的论文_一篇文章可以带你理解python中的类

    继续上一章的内容,上一章说到了python的核心部分-创建对象的各种名词的定义,包括:对象,类,多态,封装,继承,接口和内省,面向对象设计.还有不清楚的可以review一下前文.python核心部分创 ...

  5. python爱心代码动态_一篇文章教你用python画动态爱心表白

    hRf免费资源网 初级画心hRf免费资源网 学Python,感觉你们的都好复杂,那我来个简单的,我是直接把心形看作是一个正方形+两个半圆:hRf免费资源网 hRf免费资源网 于是这就很简单了,十行代码 ...

  6. 关于python论文2000字_一篇文章搞定Python全部基础知识

    前言: 1.Python软件安装 第一章.字符串及数字变量 1.变量 2.数字型数据 要点提炼:这下面那张图就行,至于其它的,就是文本转字数(int),数字转文本(Str) 3.字符串 要点提炼:字符 ...

  7. python 提取网页正文_一篇文章教会你用Python爬取淘宝海量信息,把淘宝商品整理成一个表格...

    最近看了爬虫又新做了个小作品,来瞅瞅吧~~~ 正文开始 因为最近想买ipad,所以想要尝试一下吧 淘宝 上所有ipad商品做一个统计,把所有ipad商品的信息集合到一个excel里,那么使用爬虫这个程 ...

  8. jsbridge实现及原理_一篇文章了解 JsBridge

    一.jsbridge 定义 JSBridge 是一种 JS 实现的 Bridge,连接着桥两端的 Native 和 H5.它在 APP 内方便地让 Native 调用 JS,JS 调用 Native ...

  9. python dict 排序原理_第一章Python数据结构和算法(字典排序)

    字典排序 问题 你想创建一个字典,并且在迭代或序列化这个字典的时候能够控制元素的顺序. 解决方案 为了能控制一个字典中元素的顺序,你可以使用 collections 模块中的OrderedDict 类 ...

  10. python深拷贝实现原理_从底层剖析Python深浅拷贝

    拷贝的用途 拷贝就是copy,目的在于复制出一份一模一样的数据.使用相同的算法对于产生的数据有多种截然不同的用途时就可以使用copy技术,将copy出的各种副本去做各种不同的操作. 值得一提的是绝大部 ...

最新文章

  1. spring Ioc本质
  2. iOS 生成带 logo 的二维码,区域截屏保存至相册(小功能二连发 (一))
  3. facebook新无监督论文-Unsupervised Learning of Visual Features by Contrasting Cluster Assignments
  4. Part2_4 Sqlite基础操作
  5. spark_spark连接hive config
  6. 孪生三兄弟 CycleGAN, DiscoGAN, DualGAN
  7. 我看西电通院月考——学生应该做点什么?
  8. 如何设置Hyper-V的虚拟机快捷方式
  9. sublimeText OmniMarkupPreviewer 404
  10. cen7布署mysql数据库
  11. 计算机毕业设计中用python神经网络编程实现手写数字识别
  12. 潘正磊: 做最好、最美的你
  13. SoapUI 使用教程链接
  14. aix服务器端口配置文件,aix系统查看端口号
  15. 风靡健身圈的生酮饮食居然有这么多好处,受教了
  16. 谷歌翻译(英文PDF文档翻译成中文,免费无限制)
  17. linux中rpm -q命令,Linux中的RPM
  18. 笔记本电脑无法进入睡眠状态_小方法解决电脑无法进入睡眠模式问题
  19. 传说中的Linux三剑客命令之grep(三剑客老幺)别人只知道了而我却做到了
  20. MATLAB的Roberts算子与Sobel算子

热门文章

  1. BZOJ4259 残缺的字符串 【fft】
  2. vue-router: 路由传参
  3. Welcome-to-Swift-13继承(Inheritance)
  4. python实现决策树ID3算法
  5. 南阳oj 814 又见拦截导弹
  6. 怎样在安卓中实现在锁屏状态下弹出对话框,并可以震动和铃声,就像闹钟似的?...
  7. Arcgis Android API开发之离线地图
  8. 获取任务管理器中进程的Memory(Private Working Set)
  9. 2010-11-25
  10. 利用OpenCV3进行鱼眼镜头标定