2019-7-18:
由于工作原因需要使用到异步的库,这里总结一些异步的学习
为什么要用异步?
--这种网络应用通常瓶颈都在IO层面,解决等待读写的问题比提高文本解析速度来的更有性价比。
--而多进程带来的优点(cpu处理)并没有得到体现,反而创建和调度进程带来的开销要远超出它的正面效应,拖了一把后腿。即便如此,多进程带来的效益相比于之前单进程单线程的模型要好得多。(多进程和多线程除了创建的开销大之外还有一个难以根治的缺陷,就是处理进程之间或线程之间的协作问题,因为是依赖多进程和多线程的程序在不加锁的情况下通常是不可控的,而协程则可以完美地解决协作问题,由用户来决定协程之间的调度。)
参考:https://zhuanlan.zhihu.com/p/25228075
一、异步:
异步I / O框架使用非阻塞套接字在单个线程上执行并发操作。Python因为有GIL(全局解释锁)这玩意,不可能有真正的多线程的存在,因此很多情况下都会用multiprocessing实现并发,而且在Python中应用多线程还要注意关键地方的同步,不太方便,用协程代替多线程和多进程是一个很好的选择,因为它吸引人的特性:主动调用/退出,状态保存,避免cpu上下文切换等…
一个生动的例子:
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,立等水开。(同步阻塞)
老张觉得自己有点傻
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
3 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言。
普通水壶,同步;响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。
立等的老张,阻塞;看电视的老张,非阻塞。
情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
作者:愚抄
链接:https://www.zhihu.com/question/19732473/answer/23434554
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
二、早期的异步:
用gevent库的猴子补丁,gevent给予了我们一种以同步逻辑来书写异步程序的能力。当我们给程序打了猴子补丁后,Python程序在运行时会动态地将一些网络库(例如socket,thread)替换掉,变成异步的库。使得程序在进行网络操作的时候都变成异步的方式去工作,效率就自然提升很多了。
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成
Python社区也意识到Python需要一个独立的标准库来支持协程,于是就有了后来的asyncio。
三、协程Coroutine:
协程,又称作Coroutine。从字面上来理解,即协同运行的例程,它是比是线程(thread)更细量级的用户态线程,特点是允许用户的主动调用和主动退出,挂起当前的例程然后返回值或去执行其他任务,接着返回原来停下的点继续执行。yield语句实现函数执行到一半返回等会又跑到原来的地方继续执行。(其实这里我们要感谢操作系统(OS)为我们做的工作,因为它具有getcontext和swapcontext这些特性,通过系统调用,我们可以把上下文和状态保存起来,切换到其他的上下文,这些特性为coroutine的实现提供了底层的基础。操作系统的Interrupts和Traps机制则为这种实现提供了可能性)
yeild:
这里先插入生成器的知识:
学过生成器和迭代器的同学应该都知道python有yield这个关键字,yield能把一个函数变成一个generator,与return不同,yield在函数中返回值时会保存函数的状态,使下一次调用函数时会从上一次的状态继续执行,即从yield的下一条语句开始执行,这样做有许多好处,比如我们想要生成一个数列,若该数列的存储空间太大,而我们仅仅需要访问前面几个元素,那么yield就派上用场了,它实现了这种一边循环一边计算的机制,节省了存储空间,提高了运行效率。
""" 举例:斐波那契数列 yeild把这个函数变成了生成器,然后当我需要时,调用它的next方法获得下一个值 """ def fib(max): n, a, b = 0, 0, 1 while n max: yeild b a, b = b, a + b n = n + 1
使用协程的方式解决生产者-消费者模型:
#-*- coding:utf-8 def consumer(): status = True while True: n = yield status print("我拿到了{}!".format(n)) if n == 3: status = False def producer(consumer): n = 5 while n > 0: # yield给主程序返回消费者的状态 yield consumer.send(n) # 生产者调用了消费者的send()方法,把n发送给consumer(即c),在consumer中的n = yield status,n拿到的是消费者发送的数字,同时,consumer用yield的方式把状态(status)返回给消费者,注意:这时producer(即消费者)的consumer.send()调用返回的就是consumer中yield的status!消费者马上将status返回给调度它的主程序,主程序获取状态,判断是否错误,若错误,则终止循环,结束程序。 n -= 1 if __name__ == '__main__': c = consumer() # 因为consumer函数中存在yield语句,python会把它当成一个generator(生成器,注意:生成器和协程的概念区别很大,千万别混淆了两者),因此在运行这条语句后,python并不会像执行函数一样,而是返回了一个generator object c.send(None) # 将consumer(即变量c,它是一个generator)中的语句推进到第一个yield语句出现的位置,那么在例子中,consumer中的status = True和while True:都已经被执行了,程序停留在n = yield status的位置(注意:此时这条语句还没有被执行),上面说的send(None)语句十分重要,如果漏写这一句,那么程序直接报错 p = producer(c) # 像上面一样定义了producer的生成器,注意的是这里我们传入了消费者的生成器,来让producer跟consumer通信 for status in p: # 循环地运行producer和获取它yield回来的状态 if status == False: print("我只要3,4,5就行啦") break print("程序结束") """ 现在我们要让生产者发送1,2,3,4,5给消费者,消费者接受数字,返回状态给生产者,而我们的消费者只需要3,4,5就行了,当数字等于3时,会返回一个错误的状态。最终我们需要由主程序来监控生产者-消费者的过程状态,调度结束程序 """
把n发送generator(生成器)中yield的赋值语句中,同时返回generator中yield的变量(结果)。
协程和生成器的区别:
有些人会把生成器(generator)和协程(coroutine)的概念混淆,两者的区别还是很大的。
直接上最重要的区别:
-
generator总是生成值,一般是迭代的序列
-
coroutine关注的是消耗值,是数据(data)的消费者
-
coroutine不会与迭代操作关联,而generator会
-
coroutine强调协同控制程序流,generator强调保存状态和产生数据
相似的是,它们都是不用return来实现重复调用的函数/对象,都用到了yield(中断/恢复)的方式来实现。
四、asyncio:
asyncio是python 3.4中引入的异步IO库。为简单起见,您需要了解两件事:协程和事件循环。协程就像函数一样,但它们可以在代码中的某些点暂停或恢复。这用于在等待IO(例如HTTP请求)时暂停协程,并在此期间执行另一个协程。我们使用await(等同于yield from)
关键字来声明我们想要一个协程的返回值。事件循环用于协调协程的执行。
asyncio是python 3.4中新增的模块,它提供了一种机制,使得你可以用协程(coroutines)、IO复用(multiplexing I/O)在单线程环境中编写并发模型。
根据官方说明,asyncio模块主要包括了:
-
-
具有特定系统实现的事件循环(event loop);
-
数据通讯和协议抽象(类似Twisted中的部分);
-
TCP,UDP,SSL,子进程管道,延迟调用和其他;
-
Future类;
-
yield from的支持;
-
同步的支持;
-
提供向线程池转移作业的接口;
-
在Python3.5中,引入了aync&await 语法结构,通过"aync def"可以定义一个协程代码片段,作用类似于Python3.4中的@asyncio.coroutine修饰符,而await则相当于"yield from"。
举个例子~~
""" 当事件循环开始运行时,它会在Task中寻找coroutine来执行调度,因为事件循环注册了print_sum(),因此print_sum()被调用,执行result = await compute(x, y)这条语句(等同于result = yield from compute(x, y)),
因为compute()自身就是一个coroutine,因此print_sum()这个协程就会暂时被挂起,compute()被加入到事件循环中,程序流执行compute()中的print语句,打印”Compute %s + %s …”,然后执行了await asyncio.sleep(1.0),
因为asyncio.sleep()也是一个coroutine,接着compute()就会被挂起,等待计时器读秒,在这1秒的过程中,事件循环会在队列中查询可以被调度的coroutine,而因为此前print_sum()与compute()都被挂起了,因此事件循环会停下来等待协程的调度,
当计时器读秒结束后,程序流便会返回到compute()中执行return语句,结果会返回到print_sum()中的result中,最后打印result,事件队列中没有可以调度的任务了,此时loop.close()把事件队列关闭,程序结束。 """ import asyncio async def compute(x, y): print("Compute %s + %s ..." % (x, y)) await asyncio.sleep(1.0) return x + y async def print_sum(x, y): result = await compute(x, y) print("%s + %s = %s" % (x, y, result)) loop = asyncio.get_event_loop() loop.run_until_complete(print_sum(1, 2)) loop.close()