重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
asyncio是官方提供的协程的类库,从python3.4开始支持该模块
创新互联公司是一家专业提供徐水企业网站建设,专注与成都做网站、成都网站制作、H5建站、小程序制作等业务。10年已为徐水众多企业、政府机构等服务。创新互联专业网站建设公司优惠进行中。
async awiat是python3.5中引入的关键字,使用async关键字可以将一个函数定义为协程函数,使用awiat关键字可以在遇到IO的时候挂起当前协程(也就是任务),去执行其他协程。
await + 可等待的对象(协程对象、Future对象、Task对象 - IO等待)
注意:在python3.4中是通过asyncio装饰器定义协程,在python3.8中已经移除了asyncio装饰器。
事件循环,可以把他当做是一个while循环,这个while循环在周期性的运行并执行一些协程(任务),在特定条件下终止循环。
loop = asyncio.get_event_loop():生成一个事件循环
loop.run_until_complete(任务):将任务放到事件循环
Tasks用于并发调度协程,通过asyncio.create_task(协程对象)的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行。除了使用 asyncio.create_task() 函数以外,还可以用低层级的 loop.create_task() 或 ensure_future() 函数。不建议手动实例化 Task 对象。
本质上是将协程对象封装成task对象,并将协程立即加入事件循环,同时追踪协程的状态。
注意:asyncio.create_task() 函数在 Python 3.7 中被加入。在 Python 3.7 之前,可以改用 asyncio.ensure_future() 函数。
下面结合async awiat、事件循环和Task看一个示例
示例一:
*注意:python 3.7以后增加了asyncio.run(协程对象),效果等同于loop = asyncio.get_event_loop(),loop.run_until_complete(协程对象) *
示例二:
注意:asyncio.wait 源码内部会对列表中的每个协程执行ensure_future从而封装为Task对象,所以在和wait配合使用时task_list的值为[func(),func()] 也是可以的。
示例三:
在学习gevent之前,你肯定要知道你学的这个东西是什么。
官方描述gevent
gevent is a coroutine-based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev event loop.
翻译:gevent是一个基于协程的Python网络库。我们先理解这句,也是这次学习的重点——协程。
wiki描述协程
与子例程一样,协程也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。子例程的起始处是惟一的入口点,一旦退出即完成了子例程的执行,子例程的一个实例只会返回一次;协程可以通过yield来调用其它协程。通过yield方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。协程允许多个入口点,可以在指定位置挂起和恢复执行。
没看懂?没关系,我也没看懂,不过算是有点线索:子例程。
子例程
过程有两种,一种叫子例程(Subroutine),通常叫Sub;另一种叫函数(Function)。底层实现机制是一样的,区别在于,Sub只执行操作,没有返回值;Function不但执行操作,并且有返回值。用过VB的应该会比较清楚这点。(原谅我用了百度百科)说到底子例程就是过程,我们一般叫它函数。
说到函数,我就想吐槽了,不明白为什么要叫函数。很多时候我们写一个函数是为了封装、模块化某个功能,它是一个功能、或者说是一个过程。因为它包含的是类似于流程图那样的具体逻辑,先怎样做,然后怎样做;如果遇到A情况则怎样,如果遇到B情况又怎样。个人觉得还是叫过程比较好,叫做函数就让人很纠结了,难道因为回归到底层还是计算问题,出于数学的角度把它称为函数?这个略坑啊!为了符合大家的口味,我还是称之为函数好了(其实我也习惯叫函数了%_
讲到函数,我们就往底层深入一点,看看下面的代码:
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def a():
print "a start"
b()
print "a end"
def b():
print "b start"
c()
print "b end"
def c():
print "c start"
print "c end"
if __name__ == "__main__":
a()
a start
b start
c start
c end
b end
a end
对于这样的结果大家肯定不会意外的。每当函数被调用,就会在栈中开辟一个栈空间,调用结束后再回收该空间。
假设一个这样的场景:有个讲台,每个人都可以上去发表言论,但是每次讲台只能站一个人。现在a在上面演讲,当他说到“大家好!”的时候,b有个紧急通知要告诉大家,所以a就先下来让b讲完通知,然后a再上讲台继续演讲。如果用函数的思想模拟这个问题,堆栈示意图是这样的:
大家会不会发现问题,就是b通知完a继续演讲都要重新开始。因为函数在重新调用的时候,它的局部变量是会被重置的,对于之前他说的那句“大家好”,他是不会记得的(可能a的记性不好)。那有没有什么办法可以不让他重复,而是在打断之后继续呢?很简单,在他走下讲台之前记住当前说过的话。表现在函数中就是在退出之前,保存该函数的局部变量,方便在重新进入该函数的时候,能够从之前的局部变量开始继续执行。
升级版
如果你有一段代码生产数据,另外一段代码消费数据,哪个应该是调用者,哪个应该是被调用者?
例如:生产者 —— 消费者问题,先抛开进程、线程等实现方法。假设有两个函数producer和consumer,当缓冲区满了,producer调用consumer,当缓冲区空了,consumer调用producer,但是这样的函数互相调用会出什么问题?
Python
1
2
3
4
5
6
7
8
def producer():
print "生产一个"
consumer()
def consumer():
print "消费一个"
producer()
producer生产一个,缓冲区满了,consumer消费一个,缓冲区空了,producer生产一个,如此循环。会看到下面这样的图:
看起来好像不错,感觉两个函数协调运行的很好,很好的解决了生产者——消费者问题。如果真有这么好也就不会有协程的存在了,仔细分析会有两个问题:
无限次数的函数嵌套调用,而没有函数返回,会有什么样的后果?
两个函数貌似协调有序的工作,你来我往,但每次执行的都是同一个函数实例吗?
首先,上面的伪代码示例是一个无限的函数嵌套调用,没有函数返回来释放栈,栈的空间不断的在增长,直到溢出,程序崩溃。然后,看起来两个函数协调有序,事实上操作的都不是同一个实例对象,不知道下面的图能否看懂。
那什么东西有这样的能力呢?我们很快就可以想到进程、线程,但是你真的想使用进程、线程如此重量级的东西在这么简单的程序上吗?野蛮的抢占式机制和笨重的上下文切换!
还有一种程序组件,那就是协程。它能保留上一次调用时的状态,每次重新进入该过程的时候,就相当于回到上一次离开时所处逻辑流的位置。协程的起始处是第一个入口点,在协程里,返回点之后是接下来的入口点。协程的生命期完全由他们的使用的需要决定。每个协程在用yield命令向另一个协程交出控制时都尽可能做了更多的工作,放弃控制使得另一个协程从这个协程停止的地方开始,接下来的每次协程被调用时,都是从协程返回(或yield)的位置接着执行。
从上面这些你就可以知道其实协程是模拟了多线程(或多进程)的操作,多线程在切换的时候都会有一个上下文切换,在退出的时候将现场保存起来,等到下一次进入的时候从保存的现场开始,继续执行。
看下协程是怎样实现的:
Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import random
from time import sleep
from greenlet import greenlet
from Queue import Queue
queue = Queue(1)
@greenlet
def producer():
chars = ['a', 'b', 'c', 'd', 'e']
global queue
while True:
char = random.choice(chars)
queue.put(char)
print "Produced: ", char
sleep(1)
consumer.switch()
@greenlet
def consumer():
global queue
while True:
char = queue.get()
print "Consumed: ", char
sleep(1)
producer.switch()
if __name__ == "__main__":
producer.run()
consumer.run()
应用场景
我们一直都在大谈协程是什么样一个东西,却从没有提起协程用来干嘛,这个其实大家分析一下就能够知道。从上面的生产者——消费者问题应该能看出,它分别有两个任务,假设交给两个人去执行,但每次只能允许一个人行动。当缓冲区满的时候,生产者是出于等待状态的,这个时候可以将执行任务的权利转交给消费者,当缓冲区空得时候,消费者是出于等待状态的,这个时候可以将执行任务的权利转交给生产者,是不是很容易联想到多任务切换?然后想到线程?最后想到高并发?
但同学们又会问,既然有了线程为什么还要协程呢?因为线程是系统级别的,在做切换的时候消耗是特别大的,具体为什么这么大等我研究好了再告诉你;同时线程的切换是由CPU决定的,可能你刚好执行到一个地方的时候就要被迫终止,这个时候你需要用各种措施来保证你的数据不出错,所以线程对于数据安全的操作是比较复杂的。而协程是用户级别的切换,且切换是由自己控制,不受外力终止。
总结
协程其实模拟了人类活动的一种过程。例如:你准备先写文档,然后修复bug。这时候接到电话说这个bug很严重,必须立即修复(可以看作CPU通知)。于是你暂停写文档,开始去填坑,终于你把坑填完了,你回来写文档,这个时候你肯定是接着之前写的文档继续,难道你要把之前写的给删了,重新写?这就是协程。那如果是子例程呢?那你就必须重新写了,因为退出之后,栈帧就会被弹出销毁,再次调用就是开辟新的栈空间了。
总结:协程就是用户态下的线程,是人们在有了进程、线程之后仍觉得效率不够,而追求的又一种高并发解决方案。为什么说是用户态,是因为操作系统并不知道它的存在,它是由程序员自己控制、互相协作的让出控制权而不是像进程、线程那样由操作系统调度决定是否让出控制权。
协程函数:async def 函数名。3.5+
协程对象:执行协程函数()得到的协程对象。
3.5之后的写法:
3.7之后的写法:更简便
await后面 跟 可等待的对象。(协程对象,Future,Task对象 约等于IO等待)
await实例2:串行执行。 一个协程函数里面可以支持多个await ,虽然会串行,但是如果有其他协程函数,任务列表也在执行,依然会切换。只是案例中的main对应执行的others1和others2串行 。 await会等待对象的值得到之后才继续往下走。
协程也称为微线程,是在一个线程中,通过不断的切换任务函数实现了多任务的效果。
协程在python实现的原理主要是通过yield这个关键字实现
但是真正在开发时,可以不需要自己实现,可以通过很多成熟的第三方模块来实现协程,比如greenlet,gevent等模块。多线程的课程我记得是在黑马程序员里面找的,一套,还有资料。
python里推荐用多进程而不是多线程,但是多进程也有其自己的限制:相比线程更加笨重、切换耗时更长,并且在python的多进程下,进程数量不推荐超过CPU核心数(一个进程只有一个GIL,所以一个进程只能跑满一个CPU),因为一个进程占用一个CPU时能充分利用机器的性能,但是进程多了就会出现频繁的进程切换,反而得不偿失。
不过特殊情况(特指IO密集型任务)下,多线程是比多进程好用的。
举个例子:给你200W条url,需要你把每个url对应的页面抓取保存起来,这种时候,单单使用多进程,效果肯定是很差的。为什么呢?
例如每次请求的等待时间是2秒,那么如下(忽略cpu计算时间):
1、单进程+单线程:需要2秒*200W=400W秒==1111.11个小时==46.3天,这个速度明显是不能接受的
2、单进程+多线程:例如我们在这个进程中开了10个多线程,比1中能够提升10倍速度,也就是大约4.63天能够完成200W条抓取,请注意,这里的实际执行是:线程1遇见了阻塞,CPU切换到线程2去执行,遇见阻塞又切换到线程3等等,10个线程都阻塞后,这个进程就阻塞了,而直到某个线程阻塞完成后,这个进程才能继续执行,所以速度上提升大约能到10倍(这里忽略了线程切换带来的开销,实际上的提升应该是不能达到10倍的),但是需要考虑的是线程的切换也是有开销的,所以不能无限的启动多线程(开200W个线程肯定是不靠谱的)
3、多进程+多线程:这里就厉害了,一般来说也有很多人用这个方法,多进程下,每个进程都能占一个cpu,而多线程从一定程度上绕过了阻塞的等待,所以比单进程下的多线程又更好使了,例如我们开10个进程,每个进程里开20W个线程,执行的速度理论上是比单进程开200W个线程快10倍以上的(为什么是10倍以上而不是10倍,主要是cpu切换200W个线程的消耗肯定比切换20W个进程大得多,考虑到这部分开销,所以是10倍以上)。
还有更好的方法吗?答案是肯定的,它就是:
4、协程,使用它之前我们先讲讲what/why/how(它是什么/为什么用它/怎么使用它)
what:
协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其它协程共享全局数据和其它资源。
why:
目前主流语言基本上都选择了多线程作为并发设施,与线程相关的概念是抢占式多任务(Preemptive multitasking),而与协程相关的是协作式多任务。
不管是进程还是线程,每次阻塞、切换都需要陷入系统调用(system call),先让CPU跑操作系统的调度程序,然后再由调度程序决定该跑哪一个进程(线程)。而且由于抢占式调度执行顺序无法确定的特点,使用线程时需要非常小心地处理同步问题,而协程完全不存在这个问题(事件驱动和异步程序也有同样的优点)。
因为协程是用户自己来编写调度逻辑的,对CPU来说,协程其实是单线程,所以CPU不用去考虑怎么调度、切换上下文,这就省去了CPU的切换开销,所以协程在一定程度上又好于多线程。
how:
python里面怎么使用协程?答案是使用gevent,使用方法:看这里
使用协程,可以不受线程开销的限制,我尝试过一次把20W条url放在单进程的协程里执行,完全没问题。
所以最推荐的方法,是多进程+协程(可以看作是每个进程里都是单线程,而这个单线程是协程化的)
多进程+协程下,避开了CPU切换的开销,又能把多个CPU充分利用起来,这种方式对于数据量较大的爬虫还有文件读写之类的效率提升是巨大的。
为了管理协程和I/O的回调函数,asyncio库的事件循环也能基于定时的方式调用普通的函数,使用call_soon()函数,例子如下:
import asyncio
import functools
def callback(arg, *, kwarg='default'):
print('callback invoked with {} and {}'.format(arg, kwarg))
async def main(loop):
print('registering callbacks')
loop.call_soon(callback, 1)
wrapped = functools.partial(callback, kwarg='not default')
loop.call_soon(wrapped, 2)
await asyncio.sleep(0.1)
event_loop = asyncio.get_event_loop()
try:
print('entering event loop')
event_loop.run_until_complete(main(event_loop))
finally:
print('closing event loop')
event_loop.close()
结果输出如下:
entering event loop
registering callbacks
callback invoked with 1 and default
callback invoked with 2 and not default
closing event loop