在协程之前我们有什么?
协程实际上不是一个新概念,作为一个并发模型,在很早以前就能看到协程的身影。只是最近才开始变得火热起来,因为它可以很好的处理IO密集型任务,而这符合互联网行业的业务需求。
在我们重新认识协程之前,先简短回顾下几个常用的并发模型。
最简单的就是串行执行的程序,遇到某一个IO事件就会阻塞直到IO事件返回,这种程序最大的缺点就是慢,耗时约等于全部IO耗时与计算耗时之和。
为了提升运行效率,可以采用多线程或多进程模型,因为IO事件实际上是不消耗计算资源的,只需要等待而已,所以在IO事件等待的时候切换到另一个任务来提升运行速度,而这需要依靠操作系统对线程和进程的调度。
但是线程和进程的分配是需要开销的,在面对大量IO事件时,系统资源就不够用了,所以才有了 select, poll, epoll等非阻塞异步IO。但是异步程序虽然性能上非常棒,但是可读性上十分反人类,因为它对原本连贯的逻辑进行了拆分,在系统规模变得很大时,我们很难理解系统的逻辑。而协程在一定程度上可以解决这个问题,它可以在保持程序的逻辑连贯性的同时,通过对任务的调度来实现调用的异步性,同时协程由于整个程序都在一个线程内,所以上下文切换的开销极小,运行效率极高。
Python 中的协程
生成器
生成器是 Python 中的一个重要特性,在 Python2 里面协程需要在生成器的基础上进行拓展。所以先来看一个生成器的例子,这个例子是一个将大文件分块读入内存的例子,它避免了文件过大无法一次性读入内存,或者是一次性读入太慢的问题。常用的 range 也有一个生成器版 xrange,range 一次性会生成所有的数据,而 xrange则到需要时才生成。
import os
def chunked_file(file, n=100):
with open(file) as fp:
while True:
chunk = fp.read(n)
if chunk:
yield chunk.encode('utf-8')
else:
break
fp.seek(n, os.SEEK_CUR)
if __name__ == '__main__':
for chunk in chunked_file('demo.txt', 256):
print chunk
除了上面的用法,生成器还可以主动调用 next 方法获取下一个输出,在没有下一个输出的情况下会抛出 StopIteration , 前面的for循环只是一个语法糖帮我们处理了next和对抛出异常的检测。
>>> demo = chunked_file('demo.txt', 10)
>>> demo.next()
'1234567890'
>>> demo.next()
'1234567890'
...
>>> demo.next()
StopIteration:
传递数据
Python 里为生成器提供了一个 send 方法,用来向生成器发送数据。下面是一个echo函数的例子,同时可以通过调用生成器函数的close方法来关闭生成器。
# echo.py
def echo():
x = yield
while True:
x = yield x
>>> generator = echo()
>>> generator.send(None) # 启动生成器
>>> generator.send('hello')
'hello'
>>> generator.send('world')
'world'
>>> generator.close() # 关闭生成器
使用 yield 和 send 构建协程
有了这两个基础的语义,就可以在此基础上构建协程了。下面是一个简单的协程调度实现,通过对函数的切换达到并发执行的效果。
# -*- encoding=utf-8 -*-
import Queue
def countdown(n):
while n > 0:
print '[Counting Down {n}...]'.format(n=n)
n -= 1
yield
def countup(n):
x = 0
while x < n:
print '[Counting Up {n}...]'.format(n=x)
x += 1
yield
def scheduler(fn_list=(countdown(10), countdown(5), countup(5))):
queue = Queue.deque()
queue.extend(fn_list)
while len(queue):
try:
task = queue.popleft()
task.send(None)
queue.append(task)
except StopIteration:
pass
if __name__ == '__main__':
scheduler()
输出结果:
[Counting Down 10...]
[Counting Down 5...]
[Counting Up 0...]
[Counting Down 9...]
[Counting Down 4...]
[Counting Up 1...]
[Counting Down 8...]
[Counting Down 3...]
[Counting Up 2...]
......