协程
了解协程之前,需要先了解多线程和多进程以及函数的工作模式:进程与线程
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。
函数(子程序),在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
def A():
print('a')
B()
print('aa')
return
def B():
print('b')
C()
print('bb')
return
def C():
print('c')
return
所以函数调用是通过栈实现的。函数调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和函数不同。
协程,又称微线程,纤程。英文名Coroutine。
协程看上去也是函数,但执行过程中,在函数内部可中断,然后转而执行别的函数,在适当的时候再返回来接着执行。
注意,在一个函数中中断,去执行其他函数,这并不是使用函数调用。有点类似CPU的中断。比如函数A、B:
def A():
print(1)
print(2)
print(3)
def B():
print('x')
print('y')
print('z')
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:1,2,x,y,3,z。
在A中没有调用B,所以协程看起来有点像多线程,但协程的特点在于是一个线程执行。
协程是一种用户态的轻量级线程。
“函数就是协程的一种特例——不切换的协程。”
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态。换种说法——进入上一次离开时所处逻辑流的位置。
那么和多线程比,协程有何优势和劣势?
优势:
1、最大的优势就是协程极高的执行效率。因为函数切换不同于线程切换,函数切换是由程序自身控制,因此没有线程切换的开销。和多线程相比,线程数量越多,协程的性能优势就越明显。
2、不需要多线程的锁机制,减少了锁定及同步的开销。因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
3、方便切换控制流,简化编程模型。
4、高并发+高扩展性+低成本,一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
1、无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
2、进行阻塞操作(如IO时)会阻塞掉整个程序
来个例子:
经典的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程的方法,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产:
import time
def consumer(name):
while True:
food = yield
print('%s吃了%s号食物'%(name, food))
def producter(name):
print('%s开始做吃的了'%name)
c1 = consumer('a')
c2 = consumer('b')
c1.__next__() #第一次要next一下才能走到生成器的yield位置
c2.__next__()
for i in range(100):
print('----------------------')
print('做好了%s号吃的'%i)
c1.send(i)
c2.send(i)
time.sleep(1)
c1.close()
c2.close()
producter('lkh')
Consumer属于生成器,首先调用next启动生成器,然后通过c.send(n)切换到consumer执行;consumer通过yield拿到消息进行处理,producter继续生产下一条消息。最后通过close()关闭consumer。
整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
执行结果:
但是,协程有一个标准定义:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 一个协程遇到IO操作自动切换到其它协程
基于上面这4点定义,我们刚才用yield实现的程并不能算是合格的协程。
所以在python中,协程已经封装好了。
在python中,greenlet是封装好了的协程,自带直接用,但必须手动切换执行顺序
from greenlet import greenlet
def f1():
print(12)
gr2.switch()
print(12)
def f2():
print(21)
gr1.switch()
print(22)
gr1 = greenlet(f1)
gr2 = greenlet(f2)
gr1.switch()
gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
自动io切换
import gevent
def func1():
print('11')
gevent.sleep(2)
print('12')
def func2():
print('21')
gevent.sleep(1)
print(22)
gevent.sleep(0.5)
print(23)
def func3():
print(31)
gevent.sleep(0.5)
print(32)
#输出11,21,31,32,22,23,12
gevent.joinall([
gevent.spawn(func1),
gevent.spawn(func2),
gevent.spawn(func3),
])