协程
官网定义
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
通俗理解
协程又被称为微线程,协程的完成主要靠yield关键字,协程执行过程中,在子程序内部可中断,将当前运行的上下文挂起,然后转而执行别的子程序,在适当的时候再返回来接着执行。
协程的优势
python中实现并发的方式有很多种,通过多进程并发可以真正利用多核资源,而多线程并发则实现了进程内资源的共享,然而Python中由于GIL锁机制,多线程实际上是伪多线程;
对于计算密集型程序,应该使用多进程并发充分利用多核资源,而在IO密集型程序中,多核优势并不明显,甚至由于大多数时间都是在IO堵塞状态,多进程的切换消耗反而让程序效率更加低下;
而当需要并发处理IO密集型任务时,就需要用到协程(Coroutine)。协程并没有系统级的调度,而是用户级的调度方式,避免了系统调用的开销,虽然协程最终是串行工作,但是却可以实现非常大的并发量。通过多进程+协程的方式,可以有效均衡多核计算和请求等待。
Python协程使用方法
1.带有yield关键字的函数自动变成生成器
2.生成器被调用时不会立即执行
3.对于生成器,当调用函数next(generator)时,将获得生成器yield后面表达式的值;
4.当生成器已经执行完毕时,再次调用next函数,生成器会抛出StopIteration异常
代码示范
定义一个使用yield的foo()方法
def foo():
while True:
print("======这是yield之前前前前====")
x = yield
print("x当前值:{0}".format(x))
print("======这是yield之后后后后====")
使用next()和send()
if __name__ == '__main__':
g = foo()
# 第一次调用next的时候,程序从函数最开始处运行,执行到yield处,停在该处
next(g)
print("\n=====================下 一 个====================\n")
next(g)
print("\n=====================下 一 个====================\n")
# send将参数赋给yield的返回值,然后该返回值赋给了变量x
# 继续程序的执行,直到下一次遇到yield停下来
g.send(1)
# next就相当于send(None)
print("\n=====================下 一 个====================\n")
g.send(None)
运行结果
我们可以发现,实际上就是在yield关键字的位置停止程序向下执行,同时记住当前执行到的位置,下次继续在这个位置向后执行,直到结束或者又遇到yield。
我们发现,整个程序过程中没有用到锁,因为协程只有一个线程,也不存在同时写变量冲突,所以在协程中控制共享资源不需要加锁,只需要判断状态即可。
yield实现生产者消费者
定义消费者
def cousume():
r = ''
while True:
n = yield r
if not n:
return
print("消费者说:消费者消费了第{0}个".format(n))
time.sleep(1)
r = '用了'
定义生产者
def produce(c):
next(c)
n = 0
while n < 3:
n += 1
print("生产者说:生产者生产了第{0}个:".format(n))
r = c.send(n)
print("生产者说:消费者反馈-- {0}".format(r))
c.close()
程序运行输出
if __name__ == '__main__':
c = cousume()
produce(c)
print("结束")
分析
- 首先调用consume函数。consume函数的返回是一个生成器,把这个生成器传入produce函数。
- produce函数中调用next(c)启动生成器。
- 计算n =n + 1生成数据,一旦生产了数据,调用c.send(n)切换到consume执行。
- consume函数中拿到数据后赋值给n,继续执行yield后面的语句
- consume函数中打印消费的数据,并设置返回值r,又回到循环的开始,通过yield把结果传回。
- produce拿到consume返回的值,继续生产下一个数据。
- 数据生产完毕后,循环结束,通过c.close()关闭consume,结束全过程
注意
produce和consume函数是在一个线程内执行,通过调用send方法和yield互相切换