协程Coroutines
协程是什么
协程,Coroutines,也叫作纤程(Fiber)
协程,全称是“协同程序”,用来实现任务协作。是一种在线程中,比线程更加轻量级的存在,由程序员自己写程序来管理。
当出现IO阻塞时,CPU一直等待IO返回,处于空转状态。这时候用协程,可以执行其他任务。当IO返回结果后,再回来处理数据。充分利用了IO等待的时间,提高了效率。
一个故事说明进程、线程、协程的关系
乔布斯想开工厂生产手机,费劲力气,制作一条生产线,这个生产线上有很多的器件以及材料。一条生产线就是一个进程。
只有生产线是不够的,所以找五个工人来进行生产,这个工人能够利用这些材料最终一步步的将手机做出来,这五个工人就是五个线程。
为了提高生产率,想到3种办法:
- 一条生产线上多招些工人,一起来做手机,这样效率是成倍増长,即单进程多线程方式
- 多条生产线,每个生产线上多个工人,即多进程多线程
- 乔布斯深入一线发现工人不是那么忙,有很多等待时间。于是规定:如果某个员工在等待生产线某个零件生产时 ,不要闲着,干点其他工作。也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,这就是:协程方式。
协程的核心(控制流的让出和恢复)
- 每个协程有自己的执行栈,可以保存自己的执行现场
- 可以由用户程序按需创建协程(比如:遇到io操作)
- 协程“主动让出(yield)”执行权时候,会保存执行现场(保存中断时的寄存器上下文和栈),然后切换到其他协程
- 协程恢复执行(resume)时,根据之前保存的执行现场恢复到中断前的状态,继续执行,这样就通过协程实现了轻量的由用户态调度的多任务模型
协程和多线程比较
比如,有3个任务需要完成,每个任务都在等待I/O操作时阻塞自身。阻塞在I/O操作上所花费的时间已经用灰色框标示出来了。
- 在单线程同步模型中,任务按照顺序执行。如果某个任务因为I/O而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。
- 多线程版本中,这3个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。
- 协程版本的程序中,3个任务交错执行,但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时,注册一个回调到事件循环中,然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。
协程的优点
- 由于自身带有上下文和栈,无需线程上下文切换的开销,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级;
- 无需原子操作的锁定及同步的开销;
- 方便切换控制流,简化编程模型
- 单线程内就可以实现并发的效果,最大限度地利用cpu,且可扩展性高,成本低(注:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理)
asyncio协程是写爬虫比较好的方式。比多线程和多进程都好. 开辟新的线程和进程是非常耗时的。
协程的缺点
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上。
- 当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
使用yield实现协程(已淘汰,了解即可)
Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:
- 最初的生成器变形
yield/send
- 引入
@asyncio.coroutine
和yield from
- Python3.5版本后,引入
async/await
关键字
【示例】不使用协程执行多个任务
#coding=utf-8
import time
def func1():
for i in range(3):
print(f'北京:第{i}次打印啦')
time.sleep(1)
return "func1执行完毕"
def func2():
for k in range(3):
print(f'上海:第{k}次打印了' )
time.sleep(1)
return "func2执行完毕"
def main():
func1()
func2()
if __name__ == '__main__':
start_time = time.time()
main()
end_time = time.time()
print(f"耗时{end_time-start_time}") #不使用协程,耗时6秒
'''
执行结果:
北京:第0次打印啦
北京:第1次打印啦
北京:第2次打印啦
上海:第0次打印了
上海:第1次打印了
上海:第2次打印了
耗时6.042387008666992
'''
【示例】使用yield协程,实现任务切换
#coding=utf-8
import time
def func1():
for i in range(3):
print(f'北京:第{i}次打印啦')
yield # 只要方法包含了yield,就变成一个生成器, 开始执行func2
time.sleep(1)
def func2():
g = func1() # func1是一个生成器,func1()就不会直接调用,需要通过next()或for循环调用
print(type(g))
for k in range(3):
print(f'上海:第{k}次打印了' )
next(g) # 转换执行func1的代码
time.sleep(1)
#有了yield,我们实现了两个任务的切换+保存状态
if __name__ == '__main__':
start_time = time.time()
func2()
end_time = time.time()
print(f"耗时{end_time-start_time}") #耗时5.0秒,效率差别不大
'''
执行结果:
上海:第0次打印了
北京:第0次打印啦
上海:第1次打印了
北京:第1次打印啦
上海:第2次打印了
北京:第2次打印啦
耗时5.039638996124268
'''
asyncio实现协程(重点)
- 正常的函数执行时是不会中断的,所以你要写一个能够中断的函数,就需要加
async
async
用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5)
)消失后,也就是5秒到了再回来执行await
用来用来声明程序挂起,比如异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。asyncio
是python3.5之后的协程模块,是python实现并发重要的包,这个包使用事件循环驱动实现并发。
使用asyncio,整体执行完,耗时3秒,效率极大提高。
【示例】asyncio异步IO的典型使用方式
#coding=utf-8
import asyncio
import time
async def func1(): #async表示方法是异步的
for i in range(3):
print(f'北京:第{i}次打印啦')
await asyncio.sleep(1)
return "func1执行完毕"
async def func2():
for k in range(3):
print(f'上海:第{k}次打印了' )
await asyncio.sleep(1)
return "func2执行完毕"
async def main():
res = await asyncio.gather(func1(), func2())
#await异步执行func1方法
#返回值为函数的返回值列表,本例为["func1执行完毕", "func2执行完毕"]
print(res)
if __name__ == '__main__':
start_time = time.time()
asyncio.run(main())
end_time = time.time()
print(f"耗时{end_time-start_time}") #耗时3秒,效率极大提高