python协程的演化
前言
Python由于众所周知的GIL
的原因,同一时刻只能有一个线程在运行,那么对于CPU密集的程序来说,线程之间的切换开销就成了拖累,而以I/O为瓶颈的程序正是协程所擅长的:
多任务并发(非并行),每个任务在合适的时候挂起(发起I/O)和恢复(I/O结束)*
Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:
- 最初的生成器进化的yield/send
- python3.4引入@asyncio.coroutine和yield from
- 在Python3.5版本中引入async/await关键字
yield/send
我们用斐波那契数列做个例子
传统的方式
1 | def normal_fib(n): |
如果我们仅仅是需要拿到斐波那契序列的第n位,或者仅仅是希望依此产生斐波那契序列,那么上面这种传统方式就会比较耗费内存。这时生成器的特性就派上用场了—> yield
!!!
yield
我们用yield
实现菲波那切数列。
1 | def gen_fib(n): |
当一个函数中包含yield
语句时,python会自动将其识别为一个生成器。这时fib(20)并不会真正调用函数体,而是以函数体生成了一个生成器对象实例。
yield
在这里可以保留gen_fib
函数的计算现场,暂停gen_fib
的计算并将b返回。而将fib放入for…in
循环中时,每次循环都会调用next(fib(20))
,唤醒生成器,执行到下一个yield
语句处,直到抛出StopIteration
异常。此异常会被for循环捕获,导致跳出循环。
send
send
事件驱动,生成器进化成协程
1 | import time |
协程更多详细信息请移步python coroutine这里~
yield from
yield from
用于重构生成器,简单的,可以这么使用:
1 | def copy_fib(n): |
这种使用方式很简单,但远远不是yield from
的全部。yield from
的作用还体现可以像一个管道一样将send
信息传递给内层协程,并且处理好了各种异常情况,因此,对于coro_fib
也可以这样包装和使用:
1 | def copy_coro_fib(n): |
asyncio/yield from
asyncio
是一个基于事件循环的实现异步I/O
的模块。通过yield from
,我们可以将协程的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒协程,接着向后执行代码。
使用asyncio.coroutine
装饰器
1 | # 并发处理两个快慢不一的斐波那契生成函数 |
运行结果如下:
1 | ... |
async/await
清楚了asyncio.coroutine
和yield from
之后,在Python3.5中引入的async
和await
就不难理解了:
可以将他们理解成asyncio.coroutine/yield from
的完美替身。当然,从Python设计的角度来说,async/await
让协程表面上独立于生成器而存在,将细节都隐藏于asyncio
模块之下,语法更清晰明了。
async/await 示例:
1 | # 使用 async/await 关键字 |
可以发现相比上面yield from
的版本只改变了以下两点:
- 函数定义前面加了
async
关键字,更加清晰表明这是一个协程 yield from
换成了await
关键字
总结
示例程序中都是以sleep为异步I/O的代表,在实际项目中,可以使用协程异步的读写网络、读写文件、渲染界面等,而在等待协程完成的同时,CPU还可以进行其他的计算。协程的作用正在于此。