APP下载

python中的异步实践与在tornado中的应用

消息来源:baojiabao.com 作者: 发布时间:2024-05-19

报价宝综合消息python中的异步实践与在tornado中的应用

最近专案中由于在python3中使用tornado,之前也有用过,是在python2中,由于对于协程理解不是很透彻,只是套用官方文件中的写法,最近比较细致的看了下协程的用法,也将tornado在python3中异步的实践了一下。

异步基础

要理解协程,先要理解异步,要理解异步,先要理解同步,与同步相关的概念又有阻塞与非阻塞,下面一一做简单介绍。

阻塞

阻塞状态指程式未得到所需计算资源时被挂起的状态。程式在等待某个操作完成期间,自身无法继续干别的事情,则称该程式在该操作上是阻塞的。

常见的阻塞形式有:网络 I/O 阻塞、磁盘 I/O 阻塞、使用者输入阻塞等。阻塞是无处不在的,包括 CPU 切换上下文时,所有的程序都无法真正干事情,它们也会被阻塞。如果是多核 CPU 则正在执行上下文切换操作的核不可被利用。

非阻塞

程式在等待某操作过程中,自身不被阻塞,可以继续执行干别的事情,则称该程式在该操作上是非阻塞的。

非阻塞并不是在任何程式级别、任何情况下都可以存在的。

仅当程式封装的级别可以囊括独立的子程式单元时,它才可能存在非阻塞状态。

非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。

同步

不同程式单元为了完成某个任务,在执行过程中需靠某种通讯方式以协调一致,称这些程式单元是同步执行的。

例如购物系统中更新商品库存,需要用“行锁”作为通讯讯号,让不同的更新请求强制排队顺序执行,那更新库存的操作是同步的。

简言之,同步意味着有序。

异步

为完成某个任务,不同程式单元之间过程中无需通讯协调,也能完成任务的方式,不相关的程式单元之间可以是异步的。

例如,爬虫下载网页。排程程式呼叫下载程式后,即可排程其他任务,而无需与该下载任务保持通讯以协调行为。不同网页的下载、储存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。

简言之,异步意味着无序。

这个概念让我想起了上学时学过的一篇文章,讲统筹安排的,比如你现在要烧水,做饭,洗衣服三件事,如果同步的进行,先烧水,在水烧开的过程中你什么都不做就等着它烧开,然后水烧开以后你再接着做饭,饭做熟的过程中你也是什么都不干,就干等著,饭做熟后再去将洗衣服放入洗衣机中去洗,之后又是干等著。如果单做一件事的时间是烧水10分别,做饭30分钟,洗衣服20分钟,那么完成这三件事总共需要60分钟。

如果将这三件事异步的去进行,我先将水烧上,然后再将衣服放到洗衣机里,然后去做饭,这三件事同时进行,当水烧开的时候给我一个讯号,这里就是水壶会响,我听到响声以后我会中止做饭这件事情去处理烧开的水,比如把它倒到保温瓶中,衣服洗完以后洗衣机也会给我一个讯号,那么我就会将衣服拿出来晾晒。这样处理完三件事总共的时间就由三件事情中最长的时间决定,这里就是30分钟,其实异步的处理就是最大程度的发挥cup的处理能力,让其在同一时间内做更多的事情。

上面的过程用程式码来实现大概是这个样子

执行结果如下

`yield` 语法

以上是用了多执行绪的方式来达到异步的效果,但是并没有用到协程,协程在python2就有,现在来看看在python2中通过yield语法。以下方法是在python2.6中执行.

要了解 yield 语法,先要了解一个概念: Generator ‘生成器’,关于generator的概念可以参考廖雪峰的教程,写的很好,生成器

如果一个函式定义中包含 yield 关键字,那么这个函式就不再是一个普通函式,而是一个 generator

执行该指令码以后程式并没有任何输出,因为它有yield表示式,因此,我们通过next()语句让它执行。next()语句将恢复Generator执行,并直到下一个yield表示式处。比如:

呼叫 c.nect() 以后,函式开始执行,这时先打印 "I am yangyanxing", 之后遇到 yield 关键字,此时函式又被中断,指令码执行结束,程式只打印了一行 "I am yangyanxing",如果想要打印出 I am fjy 呢,以时需要再呼叫一次 c.next(), 当再次呼叫 c.next() 时,函式从之前的 yield 处开始执行,由于函式在之后没有 yield了,所以程式会抛一个 StopIteration 异常。

与 next() 函式相关的还有一个 send() 函式,next 函式传递的是 None , send函式可以传递对应的值。其实next()和send()在一定意义上作用是相似的,区别是send()可以传递yield表示式的值进去,而next()不能传递特定的值,只能传递None进去。因此,我们可以看做

c.next() 和 c.send(None) 作用是一样的。看如下的程式码。

函式的输出为

I am yangyanxing

hahaha

I am fjy

这程式码解析,首先通过 c = h() 定义了一个 generator ,然后呼叫 c.next() 启动这个生成器,生成器先打印出 I am yangyanxing 然后遇到 m = yield 5 这行程式码,后停止,之后再呼叫 c.send("hahaha") ,这时候 m 的值就是 hahaha, 然后再打印出 m ,之后再打印出 I am fjy,之后又遇到了 yield 关键字,程式又中止了,整个指令码执行结束,需要提醒的是,第一次呼叫时,请使用next()语句或是send(None),不能使用send传送一个非None的值,否则会出错的,因为没有yield语句来接收这个值。

那么 next() 与 send() 函式的返回值是什么呢? 注意到上面函式中的 yield 之后是一个5了吗?其实这就是呼叫 netx 或者 send 以后得到的返回值.

得到的输出为

I am yangyanxing

5

hahaha

I am fjy

6

异步使用

同步的困扰

首先看以下的程式码,以下是在python2中编写

我分别用浏览器和和用指令码对 http://127.0.0.1:8000/?q=yangyanxing 该 url 进行访问,指令码如下

服务端显示

[I 190114 00:03:46 web:2162] 304 GET /?q=yangyanxing (127.0.0.1) 5000.97ms

[I 190114 00:03:51 web:2162] 200 GET /?q=yangyanxing (127.0.0.1) 5006.78ms

指令码打印为 7或者8

在同步应用中,由于同时只能提供一个请求。所以,如果一个路由中有一个比较耗时的操作,如程式码中的 time.sleep(5) 那么意味着如果同时有两个请求,那么第二个请求只能等待服务器处理完第一个请求之后才能处理第二个请求,也就中处理两个请求,最短要5秒,最长要10秒,这还只是2个,如果有10个那就是要50秒处理完所有的请求,这个效率是无法接受的,服务端可否同时处理10个请求,就像文章一开始提到的同时做三件事情,在处理一个耗时的操作时,不是干等著这件事情处理完,而是去做别的事情,当那件事情结束以后,再通过呼叫回拨函式来通知呼叫者。

异步的使用

客户端的实现异步的使用可以分为客户端的呼叫与服务端的处理,先从简单的来看,客户端的呼叫,比如你要同时访问 baidu.com 10次,你会怎么做?可以依次的对 baidu 发起10次请求,每次请求结束以后再发起下一次请求,假如每次请求是1秒钟,那么10次请求至少要用10秒钟,排除IO相关耗时,程式码可能是这个样子的

执行结果如下:

一共用了0.7秒,百度的反应可能太快了,我们准备一个本地的环境来模拟慢返回。这里我先使用tornado的异步协程处理,之后再详细说明该处的用法。

请求程式码改为三次,只是为了说明问题

结果:

用了5.009501695632935时间

用了5.012002229690552时间

用了5.012502193450928时间

all done,use 15.035006284713745 time

可以看到,总是时间是15秒,同步对一个url发请求,在没有做异步处理的时候时间是累积的。

接下来说本篇的重点,协程.

定义协程在一个普通的函式前面加上 async 关键字,此时该函式会返回一个coroutine物件,函式里也不会立刻执行.

执行结果:

此处的 s 是一个coroutine物件,那么怎么才能执行函式里面的方法呢? 将这个协程物件放到事件循环 event_loop 中执行

执行结果:

用了5.009500026702881时间

如果同时发三个请求呢?

这里要对协程做一个封装,将其封装成一个 task 物件

结果如下:

总的时间还是15秒,并没有实现异步呢,还是同步的依次执行请求。

其实,要实现异步处理,我们得先要有挂起的操作,当一个任务需要等待 IO 结果的时候,可以挂起当前任务,转而去执行其他任务,这样我们才能充分利用好资源,上面方法都是一本正经的序列走下来,连个挂起都没有,怎么可能实现异步?

上面的函式存在耗时的操作就是r = requests.get(url) 那么将它写成挂起的呢?

r = await requests.get(url)

不出意外的报错了

Task exception was never retrieved

future: exception=TypeError("object Response can't be used in 'await' expression",)>

Traceback (most recent call last):

File "C:Python35libasyncio asks.py", line 240, in _step

result = coro.send(None)

File "E:/Github/asyncTorMysql/asynctest.py", line 71, in getUrlByCor

r = await requests.get(url)

TypeError: object Response can't be used in 'await' expression

这个错误的意思是 requests 返回的 Response 物件不能和 await 一起使用,await 后面的物件必须是如下格式之一

原生 coroutine 物件一个由 types.coroutine() 修饰的生成器,这个生成器可以返回 coroutine 物件。一个包含 __await 方法的物件返回的一个迭代器。reqeusts 返回的 Response 不符合上面任一条件,因此就会报上面的错误了。

既然 await 后面的物件要是 coroutine 物件 ,那么将其包装在async 后面不就可以了吗?

这次不报错了,但是依然没有异步的执行

用了5.0090014934539795时间

用了5.011002063751221时间

用了5.011502027511597时间

use 15.03450632095337 time

Task Result: hello yangyanxing

Task Result: hello yangyanxing

Task Result: hello yangyanxing

也就是说我们仅仅将涉及 IO 操作的程式码封装到 async 修饰的方法里面是不可行的!我们必须要使用支援异步操作的请求方式才可以实现真正的异步,所以这里就需要 aiohttp 派上用场了

aiohttp 是一个支援异步请求的库,利用它和 asyncio 配合我们可以非常方便地实现异步请求操作。

执行结果:

用了5.006500005722046时间

用了5.006499767303467时间

用了5.006500005722046时间

use 5.008500099182129 time

Task Result: hello yangyanxing

Task Result: hello yangyanxing

Task Result: hello yangyanxing

这次终于实现了异步请求。

还记得最开始的洗衣做饭的例子吗?可以使用异步协程来实现,程式码大概是这个样子

执行结果:

开始烧水了

开始洗衣服了

开始做饭了

水开了 一共用了2.0 时间

衣服洗完了 一共用了4.003999948501587 时间

饭熟了 一共用了9.994499921798706 时间

服务端的实现

先看下tornado在python2中的解决方案.我们再来翻过头来看之前用tornado写的服务端同步程式码

在 IndexHandler 中的 get 方法,由于当中存在了一个比较耗时的操作,time.sleep(5) 处理完这个请求需要卡5秒,在卡住的这段时间,tornado无法再完成别的请求,如果此时再发来一个 / 的请求,那么只能等待这前的请求操作结束之后再对处理新发过来的请求,如果同时有1万个请求发过来,可想而知,最后一个请求就等到猴年马月才能处理完呢……

解决方法是使用@tornado.web.asynchronous 和@tornado.gen.coroutine 装饰器,将耗时的操作放到执行绪中去执行,这里的耗时操作 time.sleep(5) 是阻塞的,所以将阻塞函式放加上 @run_on_executor 装饰器

注意到在 IndexHandler 中有一行初始化 executor 的程式码 executor = ThreadPoolExecutor(100) 这里的引数100是最大的执行绪数,我这里传的是100,也就意味着同时能处理100个请求,当有101个请求的时候,前100个请求可以同时在2秒内执行,最后的那一个请求就要等之前有结束的执行绪以后再去执行了。

再看下tornado在python3.5 中的解决方案由于在python3.5以后引入了 asyncio这个标准库,很多异步的操作可以用这个库来操作

IndexHandler 中的 get 方法使用了async 与await 关键字来达到异步的处理请求,这里的asyncio.sleep(5) 是异步的暂停5秒,如果此处的方法涉及到无法使用异步请求的库该怎么处理,比如说我就想使用time.sleep(5) 则需要线上程池中执行,就像上面的/ 路由里使用 @run_on_executor 中执行。

结语

异步操作涉及的知识点比较多,不同版本的 python 对于异步的处理也不一样,有些东西如 yield 理解起来比较费劲,需要多在专案中实践,tornado 这个框架的设计初衷也是异步网络库,过使用非阻塞网络I/O, Tornado 可以支援上万级的连线,所以要使用过程中要多多考虑异步非阻塞的编码。

参考文章

爬虫速度太慢?来试试用异步协程提速吧!

Python3 异步协程函式async具体用法

Python天天美味(25) - 深入理解yield

理解Python协程:从yield/send到yield from再到async/await

使用tornado让你的请求异步非阻塞

由于今日头条上发的文章对于程式码排版不太方便,所以我将程式码片段都使用了截图的方式,想要复制程式码请点选 "了解更多"来检视原文或者微信搜寻公众号"序语程言"

2019-09-03 09:54:00

相关文章