python协程实现--yield,yield from,greenlet,gevent

之前有讲过协程,协程就是在一个线程里面实现并发。注意和return区分开就好理解了

通俗的讲,协程在yield之后会保存当前代码的运行状态,切换到send处

专业的讲:协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

协程能够实现异步IO


协程评价

  • 优点:
    • 无需切换线程上下文,避免无意义的调度,程序员需要自己承担调度的责任,而且失去了使用多核的能力,线程越多,协程优势越明显
    • 无需原子操作锁和同步,因为只有一个线程,不存在同时写一个对象的问题,控制共享资源不需要加锁,只需要判断即可
    • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理
  • 缺点:
    • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用,但是实际中大部分Web都是IO密集型的
    • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序,究其本质,我们会发现,协程就是单线程的程序,只是能够按照我们设定的顺序执行代码。不像多线程或者多进程,可以给每一个线程绑定一个函数,分别执行。
    • 所以协程要处理并发终究是要有个事件循环的,这和我之前的博文讲到轮询很像,和事件驱动模型则不像

通过yield和yield from 配合send来实现协程

import time

def fib(n):
    index = 0
    a = 0
    b = 1

    while index < n:
        sleep = yield b
        print('等待%s秒' %sleep)
        time.sleep(sleep)
        a,b = b, a+b
        index += 1

fib = fib(20)
print(fib.send(None))   # 效果等同于print(next(fib))
print(fib.send(2))
print(fib.send(2))
print(fib.send(2))
print(fib.send(2))

当出现生成器嵌套时:虽然也可以用yield,但是用yield from是更明智的选择,yield from为我们考虑了很多非正常情况的处理

 

yield实现生成器嵌套的情况:

def fun_inner():
    i = 0
    while True:
        i = yield i

def fun_outer():
    a = 0
    b = 1
    inner = fun_inner()
    inner.send(None) # 启动fun_inner生成器
    while True:
        a = inner.send(b)
        b = yield a

if __name__ == '__main__':
    outer = fun_outer()
    outer.send(None)# 启动fun_outer生成器
    for i in range(5):
        print(outer.send(i))

yield from实现生成器嵌套:委托生成器fun_outer只需要转发主程序send的数据就行了,相当于一个管道的作用

def fun_inner():
    i = 0
    while True:
        i = yield i

def fun_outer():
    yield from fun_inner()

if __name__ == '__main__':
    outer = fun_outer()
    outer.send(None)
    for i in range(5):
        print(outer.send(i))

通过greenlet来实现

Python的 greenlet就相当于手动切换,去执行别的子程序,在“别的子程序”中又主动切换回来。greenlets之间切换通过调用greenlet的switch()方法,在这种情况下,执行点跳转到调用switch()的greenlet,当一个greenlet挂点时,执行点会跳到其父greenlet。在切换的时候,一个对象或者异常可以传到目标greenlet,这可以很方便的用来在greenlet之间传递信息
 

from greenlet import greenlet
import time


def productor():
    n = 0
    while n < 2:
        n += 1
        print("生产者正在生产数据 {}".format(n))
        c.switch(n)  # 切换到c 并传了一个参数item
        time.sleep(1)
 

def consumer():
    while True:
        data = p.switch() # 切换到p 在恢复的时候接收数据
        print("消费者 {}".format(data))
 

if __name__ == '__main__':
    c = greenlet(consumer) #将一个普通函数变成协程
    p = greenlet(productor)
    c.switch() #进入消费者函数执行,到yield后进入暂停状态,只有恢复时才能接收数据

通过gevent来实现

gevent的基本原理来自于libevent&libev,本质上libevent或者说libev都是一种事件驱动模型libevent和libev是基于epoll的,因为epoll只能在Linux下面使用,程序可移植性不好,所以就有了libevent。这种模型对于提高cpu的运行效率,增强用户的并发访问非常有效。上面也说了,普通实现的协程其实就是需要事件循环才能处理并发,但是理想的应该是事件驱动才好。比如select/poll--->epoll就是循环监听到事件驱动的进步(水平触发或者边沿触发)。

gevent:

  • gevent是在greenlet的基础上进行封装使得gevent变得更加的易用。
  • gevent采用了隐式启动事件循环,即在需要阻塞的时候开启一个专门的协程来启动事件循环
  • 如果一个任务没有io操作,那么他会一直执行直到完成;其他协程没有执行的机会,因为gevent会自动识别阻塞,有阻塞就会启动隐式事件循环,没有阻塞那就不切换(没有time,sleep),那就是顺序执行func1,func2,func3
  • 自动识别io事件,放弃CPU控制时间

优点:

  • 使用基于 epoll 的 libev 来避开阻塞,看哪个有事件触发,就执行哪个,不用轮询
  • 使用基于 gevent 的 高效协程 来切换执行
  • 只在遇到阻塞的时候切换,没有轮询的开销,也没有线程的开销,除非监听的全部没有事件触发,那么整个阻塞
import gevent


def func1():
    print("func1 running")
    gevent.sleep(1)             # 内部函数模拟实现io操作
    print("switch func1")
 

def func2():
    print("func2 running")
    gevent.sleep(1)
    print("switch func2")

 
def func3():
    print("func3  running")
    gevent.sleep(1)
    print("func3 done..")


gevent.joinall([gevent.spawn(func1),
                gevent.spawn(func2),
                gevent.spawn(func3),
                ])

其实是不是和事件循环的asyncio很像?

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读