之前有讲过协程,协程就是在一个线程里面实现并发。注意和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很像?