什么是Coroutine?
Coroutine,又称作协程。从字面上来理解,即协同运行的例程,它是比是线程(thread)更细量级的用户态线程,特点是允许用户的主动调用和主动退出,挂起当前的例程然后返回值或去执行其他任务,接着返回原来停下的点继续执行。等下,这是否有点奇怪?我们都知道一般函数都是线性执行的,不可能说执行到一半返回,等会儿又跑到原来的地方继续执行。但一些熟悉python(or其他动态语言)的童鞋都知道这可以做到,答案是用yield语句。其实这里我们要感谢操作系统(OS)为我们做的工作,因为它具有getcontext和swapcontext这些特性,通过系统调用,我们可以把上下文和状态保存起来,切换到其他的上下文,这些特性为coroutine的实现提供了底层的基础。操作系统的Interrupts和Traps机制则为这种实现提供了可能性,因此它看起来可能是下面这样的:

栗子?
理解生成器(generator)
学过生成器和迭代器的同学应该都知道python有yield这个关键字,yield能把一个函数变成一个generator,与return不同,yield在函数中返回值时会保存函数的状态,使下一次调用函数时会从上一次的状态继续执行,即从yield的下一条语句开始执行,这样做有许多好处,比如我们想要生成一个数列,若该数列的存储空间太大,而我们仅仅需要访问前面几个元素,那么yield就派上用场了,它实现了这种一边循环一边计算的机制,节省了存储空间,提高了运行效率。
这里以斐波那契数列为例:
def fib(max):
n, a, b = 0, 0, 1
while n max:
print b
a, b = b, a + b
n = n + 1
如果使用上述的算法,那么我每一次调用函数时,都要耗费大量时间循环做重复的事情。而如果使用yield的话,它则会生成一个generator,当我需要时,调用它的next方法获得下一个值,改动的方法很简单,直接把print改为yield就OK。
def fib(max):
n, a, b = 0, 0, 1
while n max:
yield b
a, b = b, a + b
n = n + 1
生产者-消费者的协程
OK,现在理解yield语句的作用了吗?
下面这个例子是典型的生产者-消费者问题,我们用协程的方式来实现它。首先从主程序中开始看,第一句c = consumer(),因为consumer函数中存在yield语句,python会把它当成一个generator(生成器,注意:生成器和协程的概念区别很大,等会再说,千万别混淆了两者),因此在运行这条语句后,python并不会像执行函数一样,而是返回了一个generator object。
再看第二条语句c.send(None),这条语句的作用是将consumer(即变量c,它是一个generator)中的语句推进到第一个yield语句出现的位置,那么在例子中,consumer中的status = True和while True:都已经被执行了,程序停留在n = yield status的位置(注意:此时这条语句还没有被执行),上面说的send(None)语句十分重要,如果漏写这一句,那么程序直接报错,这个send()方法看上去似乎挺神奇,等下再讲他的作用。
下面第三句p = producer(c),这里则像上面一样定义了producer的生成器,注意的是这里我们传入了消费者的生成器,来让producer跟consumer通信。
第四句for status in p:,这条语句会循环地运行producer和获取它yield回来的状态。
好了,进入正题,现在我们要让生产者发送1,2,3,4,5给消费者,消费者接受数字,返回状态给生产者,而我们的消费者只需要3,4,5就行了,当数字等于3时,会返回一个错误的状态。最终我们需要由主程序来监控生产者-消费者的过程状态,调度结束程序。
现在程序流进入了producer里面,我们直接看yield consumer.send(n),生产者调用了消费者的send()方法,把n发送给consumer(即c),在consumer中的n = yield status,n拿到的是消费者发送的数字,同时,consumer用yield的方式把状态(status)返回给消费者,注意:这时producer(即消费者)的consumer.send()调用返回的就是consumer中yield的status!消费者马上将status返回给调度它的主程序,主程序获取状态,判断是否错误,若错误,则终止循环,结束程序。上面看起来有点绕,其实这里面generator.send(n)的作用是:把n发送generator(生成器)中yield的赋值语句中,同时返回generator中yield的变量(结果)。
于是程序便一直运作,直至consumer中获取的n的值变为3!此时consumer把status变为False,最后返回到主程序,主程序中断循环,程序结束。(观察输出结果,是否如你所想?)
这种协程的方式保证了程序以有序地方式协作运行例程,而且它还能保存函数中的上下文状态(看看status),使每次重新进入时都能获取以前上下文的变量值。
#-*- coding:utf-8
def consumer():
status = True
while True:
n = yield status
print("我拿到了{}!".format(n))
if n == 3:
status = False
def producer(consumer):
n = 5
while n > 0:
# yield给主程序返回消费者的状态
yield consumer.send(n)
n -= 1
if __name__ == '__main__':
c = consumer()
c.send(None)
p = producer(c)
for status in p:
if status == False:
print("我只要3,4,5就行啦")
break
print("程序结束")
输出结果:
我拿到了5!
我拿到了4!
我拿到了3!
我只要3,4,5就行啦
程序结束
Coroutine与Generator
有些人会把生成器(generator)和协程(coroutine)的概念混淆,我以前也会这样,不过其实发现,两者的区别还是很大的。
直接上最重要的区别:
- generator总是生成值,一般是迭代的序列
- coroutine关注的是消耗值,是数据(data)的消费者
- coroutine不会与迭代操作关联,而generator会
- coroutine强调协同控制程序流,generator强调保存状态和产生数据
相似的是,它们都是不用return来实现重复调用的函数/对象,都用到了yield(中断/恢复)的方式来实现。
Coroutine的应用
管道
下面展示如何用coroutine实现管道。

首先了解管道的要素:
- 要有起始的源(source)

- 要有终点(end-point)

首先,我们在source建立一个模拟Unix的tail -f命令的coroutine。
import time
def follow(thefile, target):
thefile.seek(0,2) # 到达文件的底部
while True:
line = thefile.readline()
if not line:
time.sleep(0.1)
continue
target.send(line)
它接受一个文件以及一个coroutine对象作为参数,用send()把文件每一行传递到管道一侧的coroutine中。
现在,我们再为管道接上终点(end-point),也就是下面的printer(),它将负责打印行结果。
这里定义了coroutine作为装饰器,帮我们做了send(None)的操作,免去了我们的手动调用。
def coroutine(func):
def wrapper(*args, **kws):
cr = func(*args, **kws)
cr.send(None)
return cr
return wrapper
@coroutine
def printer():
while True:
line = (yield)
print line
注意了,目前为止,我们的管道是这样的:follow -> printer。
下面我们尝试使用它,跟踪读取一个日志文件:
if __name__ == '__main__':
f = open("access-log")
follow(f, printer())
而现在,我们在管道中间定义一个过滤器,同样的,模拟的是UNIX中grep的功能。
def grep(pattern):
print("Looking for %s" % pattern)
while True:
line = (yield)
if pattern in line:
print line,
到了这里,我们的管道是这样的:follow -> grep -> printer。
使用它:
if __name__ == '__main__':
f = open("access-log")
follow(f,
grep('python',
printer()))
利用coroutine我们可以很便捷地根据需要来拼接管道,定制不同的功能,十分灵活。

我们甚至可以用多管道的方式,分支查找数据。
• A more disturbing variation...
f = open("access-log")
p = printer()
follow(f,
broadcast([grep('python',p),
grep('ply',p),
)
grep('swig',p)])
事件处理
用coroutine使事件处理的方式更加简单。
试想一下Python中的XML Parser,里面就有Event Handling的概念。转换成coroutine way,我们可以借助send()方法,接受和处理Handler中发来的数据。

下面演示了一个爬取公交车信息的例子,它使用了xml.sax
来解析读取的xml信息,其中用到了事件处理和数据过滤。
@coroutine
def text_collector(target):
while True:
while True:
event, value = (yield)
if event == 'start' or event == 'end':
target.send((event,value))
else:
chunks = [value]
while True:
event, value = (yield)
if event != 'text': break
chunks.append(value)
target.send(('text',"".join(chunks)))
target.send((event,value))
@coroutine
def buses_to_dicts(target):
while True:
event, value = (yield)
# Look for the start of a element
if event == 'start' and value[0] == 'bus':
busdict = { }
# Capture text of inner elements in a dict
while True:
event, value = (yield)
if event == 'text':
textvalue = value
elif event == 'end':
if value != 'bus':
busdict[value] = textvalue
else:
target.send(busdict)
break
@coroutine
def filter_on_field(fieldname,value,target):
while True:
d = (yield)
if d.get(fieldname) == value:
target.send(d)
@coroutine
def bus_locations():
while True:
bus = (yield)
print "%(route)s,%(id)s,\"%(direction)s\","\
"%(latitude)s,%(longitude)s" % bus
if __name__ == '__main__':
import xml.sax
from cosax import EventHandler
xml.sax.parse("allroutes.xml",
EventHandler(
text_collector(
buses_to_dicts(
filter_on_field("route","22",
filter_on_field("direction","North Bound",
bus_locations()))))
))
异步IO
Coroutine的机制使得我们可以用同步的方式写出异步运行的代码。
总所周知,Python因为有GIL(全局解释锁)这玩意,不可能有真正的多线程的存在,因此很多情况下都会用multiprocessing实现并发,而且在Python中应用多线程还要注意关键地方的同步,不太方便,用协程代替多线程和多进程是一个很好的选择,因为它吸引人的特性:主动调用/退出,状态保存,避免cpu上下文切换等…一些时候,性能甚至能超越多线程的实现方式。
更简单地实现协程
asyncio
asyncio是python 3.4中新增的模块,它提供了一种机制,使得你可以用协程(coroutines)、IO复用(multiplexing I/O)在单线程环境中编写并发模型。
asyncio模块主要包括了:
- 具有特定系统实现的事件循环(event loop);
- 数据通讯和协议抽象(类似Twisted中的部分);
- TCP,UDP,SSL,子进程管道,延迟调用和其他;
- Future类;
- yield from的支持;
- 同步的支持;
- 提供向线程池转移作业的接口;
下面来看下asyncio的一个例子:
import asyncio
async def compute(x, y):
print("Compute %s + %s ..." % (x, y))
await asyncio.sleep(1.0)
return x + y
async def print_sum(x, y):
result = await compute(x, y)
print("%s + %s = %s" % (x, y, result))
loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()
当事件循环开始运行时,它会在Task中寻找coroutine来执行调度,因为事件循环注册了print_sum(),因此print_sum()被调用,执行result = await compute(x, y)这条语句(等同于result = yield from compute(x, y)),因为compute()自身就是一个coroutine,因此print_sum()这个协程就会暂时被挂起,compute()被加入到事件循环中,程序流执行compute()中的print语句,打印”Compute %s + %s …”,然后执行了await asyncio.sleep(1.0),因为asyncio.sleep()也是一个coroutine,接着compute()就会被挂起,等待计时器读秒,在这1秒的过程中,事件循环会在队列中查询可以被调度的coroutine,而因为此前print_sum()与compute()都被挂起了,因此事件循环会停下来等待协程的调度,当计时器读秒结束后,程序流便会返回到compute()中执行return语句,结果会返回到print_sum()中的result中,最后打印result,事件队列中没有可以调度的任务了,此时loop.close()把事件队列关闭,程序结束。
Gevent
如果觉得asyncio还不够简便,那么gevent将使你的最佳选择。gevent是一个基于libv的封装了greenlet的网络库,主要用于协程以及并发的处理。
gevent的基本原理:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
更多可了解gevent。