最近在复习并发编程相关知识,顺带着就学了下python的协程,觉得值得写一篇文章记录下。
协程是一个非常有意思的概念,其设定非常类似于线程,不过比线程的调度更轻量。线程的调度依赖于操作系统,可以并发(parallelism)执行;协程由应用层调度,并不会利用多CPU,其实质是在应用层维持运行队列,单进程并行(concurrency)运行。
那么协程为什么有用呢(至少在python中非常有用),有几点原因:
- 多I/O并发场景,可能并不需要太多CPU,但确会阻塞进程。
- 在python中如果I/O多的场景,可以用多线程来实现并发,但众所周知,python的GIL使得多线程等价于单进程,只是不会阻塞主进程而已。
- 而协程刚好满足了需求,即轻量,又不阻塞,又不存在创建线程的开销。
其实,很多的I/O框架通过callback大法来实现异步调用,不过使用callback并不方便:
- 非常复杂的业务场景可能嵌套多个callback,写起来非常蛋疼,而且丑陋!
- 嵌套的callback会引入闭包,而闭包使用不当会存在很多诡异问题。(比如python闭包的lazy evaluation)
所以,协程从应用层面上给出了一个非常好而且漂亮的解决方案:
- 不用callback,等待事件时,原地暂停执行。
- 特定事件完成时,调度器调用对应协程继续执行。
在python2.x时代,可以使用tornado提供的协程实现,只是使用起来会相对受限,毕竟是上层的tricky hook。
python3.5后,使用async和await两个关键字来实现coroutine的底层语法。其实质是对之前的coroutine和yield from进行包装,实现语法级别的支持。
asyncio
是携程的核心库,其中有两个核心概念:
- Future。该对象提供了一种同步机制:等待Future的语句将暂停执行,直到
Future.set_result
。其底层就是通过Future.add_done_callback
将callback进行封装,比如await future语句,其等价于定义了一个callback,在future完成后,将当前的coroutine重新放入到执行队列中,等待下一次next操作。 - Task。Task本身就是一个Future,同时包装了执行函数(对generator进行包装)。asyncio库在放入函数执行时,其最终都会用
asyncio.ensure_future(coroutine_obj)
来生成Task对象。Task会监控执行函数的完成状态,完成后将结果保存在Task中,同时调用Task本身的done_callback。所以考虑下面的代码:
async def f(): |
参照上面说明的结构,我自己实现了一个简单版本的协程框架,实现一下,可以对相关概念有更加深刻的认识。代码如下:
class EventLoop(object): |
更进一步,asyncio的核心逻辑都在主循环中体现,其主要做了几个事情:
- 检测schedule的状态,去掉堆顶无效节点。(延迟删除,只是标记为cancelled)
- 调用select,执行I/O
- 取出需要运行的schedule放到运行队列_ready中
- 执行所有需要执行的回调。
asyncio将所有的回调包装到handler中,有的回调是task,有的是schedule,有的是I/O相关封装的回调,非常统一。
主循环代码:
# 去掉schedule的无效数据,如果太多无效数据,直接重新来过,这样子不会导致多次无效数据的heappop开销 |