PHP前端开发

深入了解python中的协程函数

百变鹏仔 2个月前 (02-07) #Python
文章标签 函数

本篇文章给大家分享的内容是深入了解python中的协程函数 ,有着一定的参考价值,有需要的朋友可以参考一下

概念:

根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。从技术的角度来说,“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。

协程,又称为微线程,看上去像是子程序,但是它和子程序又不太一样,它在执行的过程中,可以在中断当前的子程序后去执行别的子程序,再返回来执行之前的子程序,但是它的相关信息还是之前的。

协程不同于线程,线程是抢占式的调度,而协程是协同式的调度,协程需要自己做调度。  
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

协程的优点:

  • 协程优势是极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。用来执行协程多任务非常合适。

    立即学习“Python免费学习笔记(深入)”;

  • 协程没有线程的安全问题。一个进程可以同时存在多个协程,但是只有一个协程是激活的,而且协程的激活和休眠又是程序员通过编程来控制,而不是操作系统控制的。

生成器实现协程原理

示例:

def func(n):    index=0    if index<pre class="prettyprint">输出打印:1task------5

解释说明:

  • 很明显func是一个生成器,send方法有一个参数,该参数指定的是上一次被挂起的yield语句的返回值。

  • send需要做异常处理。

  • 总的来说,send方法和next方法唯一的区别是在执行send方法会首先把上一次挂起的yield语句的返回值通过参数设定,从而实现与生成器方法的交互。但是需要注意,在一个生成器对象没有执行next方法之前,由于没有yield语句被挂起,所以执行send方法会报错。

  • send方法的参数为None时,它与next方法完全等价。

生成器实现生产者和消费者模式:

def cunsumer():    while True:        n=yield 3        if not n:            return        print('cunsumer{}'.format(n))def product(c):    c.send(None)    n=0    while n<pre class="brush:php;toolbar:false;">打印:cunsumer1product3cunsumer2product3cunsumer3product3cunsumer4product3cunsumer5product3

解释说明:

在生产者里先执行了 c.send(None),目的是先让消费者挂起,再用send传值,第一次传1,消费者那里打印1,生产者打印r是消费者yield后面的值。

greenlet 的引入

虽然CPython(标准Python)能够通过生成器来实现协程,但使用起来还并不是很方便。

与此同时,Python的一个衍生版 Stackless Python实现了原生的协程,它更利于使用。

于是,大家开始将 Stackless 中关于协程的代码  单独拿出来做成了CPython的扩展包。

这就是 greenlet 的由来,因此 greenlet 是底层实现了原生协程的 C扩展库。

代码示意:

from greenlet import greenletimport randomimport timedef Producer():    while True:        item = random.randint(0,10)        print("生产了{}".format(item))        c.switch(item)#切换到消费者,并将item传入消费者        time.sleep(1)def consumer():    print('我先执行')    #p.switch()    while True:        item = p.switch()#切换到生产者,并且等待生产者传入item        print('消费了{}'.format(item))c = greenlet(consumer)#将一个普通函数变成一个协程p = greenlet(Producer)c.switch()#让消费者先进入暂停状态(只有恢复了才能接收数据)

greenlet 的价值:

  • 高性能的原生协程

  • 语义更加明确的显式切换

  • 直接将函数包装成协程,保持原有代码风格

gevent协程

虽然,我们有了 基于 epoll 的回调式编程模式,但是却难以使用。

即使我们可以通过配合 生成器协程 进行复杂的封装,以简化编程难度。
但是仍然有一个大的问题: 封装难度大,现有代码几乎完全要重写

gevent,通过封装了 libev(基于epoll) 和 greenlet 两个库。
帮我们做好封装,允许我们以类似于线程的方式使用协程。

以至于我们几乎不用重写原来的代码就能充分利用 epoll 和 协程 威力。

代码示意:

from gevent import monkey;monkey.patch_all()#会把python标准库当中一些阻塞操作变成非阻塞import geventdef test1():    print("11")    gevent.sleep(4)#模拟爬虫请求阻塞    print("33")def test2():    print("22")    gevent.sleep(4)    print("44")gevent.joinall([gevent.spawn(test1),gevent.spawn(test2)])#joinall 阻塞当前协程,执行给定的greenlet#spawn 启动协程,参数就是函数的名字

gevent 的价值:

遇到阻塞就切换到另一个协程继续执行 !

  • 使用基于 epoll 的 libev 来避开阻塞。

  • 使用基于 gevent 的 高效协程 来切换执行。

  • 只在遇到阻塞的时候切换,没有轮需的开销,也没有线程的开销。

gevent实现并发服务器

from gevent import monkey;monkey.patch_all()  #建议放在首行,会把python标准库当中一些阻塞操作变成非阻塞import geventimport socketserver=socket.socket()server.bind(('',6666))server.listen(5)print("开始监听")def readable(con,addr):    print("客户端{}接入".format(addr))    while True:        data=con.recv(1024)        if data:            print(data)        else:            con.close()            breakwhile True:    con,addr=server.accept()    gevent.spawn(readable,con,addr)#将readable函数变为协程,并且把con和addr传入其中。

gevent 协程通信

gevent也有自己的队列。使用方式和进线程基本一样。

基于gevent和队列的生产者和消费者模式

from gevent import monkey;monkey.patch_all()import geventfrom gevent.queue import Queueimport randomdef producter(queue):    while True:        item=random.randint(0,99)        print('生产了{}'.format(item))        queue.put(item)        gevent.sleep(1)def comuser(queue):    while True:        item=queue.get()        print('消费了{}'.format(item))queue=Queue()p=gevent.spawn(producter,queue)c=gevent.spawn(comuser,queue)gevent.joinall([p,c])
打印:生产了33消费了33生产了95消费了95生产了92消费了92...