Node.js也是异步,不过通过callback来组织代码,不便于阅读。Go语言跟Python在实现异步方面比较相近,使用所谓的协程(coroutine),这样可以按照正常的逻辑思维来书写代码。
Tornado框架的两个核心基础点:一个是python的yield,一个是Linux的epoll。
python的yield是python一个特别的关键字,它将程序执行的权利交给函数调用者(yield只能出现在函数中),同时保留执行的环境,当返回的generator调用send()或者next()的时候,回到此前中断的位置,继续进行执行。就是所谓协程的语法支持。send()比next()可以在继续执行时传递一个值,这让x = yield 100这种写法成为可能。同时,generator必须以捕捉StopIteration异常来表征函数的结束,所以函数返回值不能使用return语法,很显示return的值,并不会自动传给send()函数,所以函数返回值需要raise出来,这点会让人感觉很奇怪。
下面的例子诠释了stackless的协程原理:
try:
from backports_abc import Generator as GeneratorType
except ImportError:
from types import GeneratorType
class Data(Exception):
def __init__(self, data):
self.data = data
def get_data():
x = yield [42, 43] #实际会是一些IO相关的fd监听行为
x[0] += 100
raise Data(x)
def run():
x = yield get_data()
x[1] += 100
raise Data({'arg1': x[0], 'arg2': x[1]})
def main(func):
a = func()
stack = [a]
arg = None
while True:
try:
if len(stack) == 0:
return arg
res = stack[-1].send(arg)
if isinstance(res, GeneratorType):
stack.append(res)
else:
arg = res
except Data as d:
stack.pop()
arg = d.data
except StopIteration:
pass
res = main(run)
print(res)
函数调用必须是堆栈模式的,所以stackless写法必须自己创建一个stack来保留协程函数各中断点之间的次序关系,这时候栈里压的是各Yield Point而不是函数地址。上面例子中,Data类是用来传返回值的,并不是真正的异常。
整过网络编程的话,流行的epoll是绕不过去的,因为它对于多连接中少量活跃的情形非常高效,这里的连接可以是socket fd,也可以是任何IO对应的fd,这里就不多说了。
Tornado中的ioloop就是使用epoll进行各连接读写事件监听的核心,实现于start()方法中。它是一个大的死循环,由读写事件和Timer消息进行驱动,大多数时候,它是IO waiting状态,不消耗CPU,所以它是异步的。
结合上面的yield,一层一层yield下去,最终yield到一个需要监听读写的fd,这正是epoll需要的东西。
以上两点我认为就是Tornado框架实现的基础。