python_day10 协程
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
需要强调的是:
1. python的线程属于内核级别的,即由操作系统控制调度(如单线程一旦遇到io就被迫交出cpu执行权限,切换其他线程运行)
2. 单线程内开启协程,一旦遇到io,从应用程序级别(而非操作系统)控制切换
对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优点如下:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu
要实现协程,关键在于用户程序自己控制程序切换,切换之前必须由用户程序自己保存协程上一次调用时的状态,如此,每次重新调用时,能够从上次的位置继续执行
(详细的:协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈)
为此,我们之前已经学习过一种在单线程下可以保存程序运行状态的方法,即yield,我们来简单复习一下:
- yiled可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
- send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换
#不用yield:每次函数调用,都需要重复开辟内存空间,即重复创建名称空间,因而开销很大 import time def consumer(item): # print('拿到包子%s' %item) x=11111111111 x1=12111111111 x3=13111111111 x4=14111111111 y=22222222222 z=33333333333 pass def producer(target,seq): for item in seq: target(item) #每次调用函数,会临时产生名称空间,调用结束则释放,循环100000000次,则重复这么多次的创建和释放,开销非常大 start_time=time.time() producer(consumer,range(100000000)) stop_time=time.time() print('run time is:%s' %(stop_time-start_time)) #30.132838010787964 #使用yield:无需重复开辟内存空间,即重复创建名称空间,因而开销小 import time def init(func): def wrapper(*args,**kwargs): g=func(*args,**kwargs) next(g) return g return wrapper @init def consumer(): x=11111111111 x1=12111111111 x3=13111111111 x4=14111111111 y=22222222222 z=33333333333 while True: item=yield # print('拿到包子%s' %item) pass def producer(target,seq): for item in seq: target.send(item) #无需重新创建名称空间,从上一次暂停的位置继续,相比上例,开销小 start_time=time.time() producer(consumer(),range(100000000)) stop_time=time.time() print('run time is:%s' %(stop_time-start_time)) #21.882073879241943
##################
缺点
协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
协程的定义(满足1,2,3就可称为协程):
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
yield切换在没有io的情况下或者没有重复开辟内存空间的操作,对效率没有什么提升,甚至更慢,为此,可以用greenlet来为大家演示这种切换