协程
协程是推荐在Tornado中写异步代码的方式.协程使用Python yield关键字来代替链式回调(在一些像gevent里使用的共生的轻量级的线程也被称作协程,但是Tornado中所有的协程使用显式的上下文切换,被称作异步函数)挂起和继续执行.
协程和异步代码一样简单,但是没有线程的昂贵成本。通过减少上下文切换的情况同时也可以使得并发更容易。
例子:
from tornado import gen
@gen.coroutine
def fetch_coroutine(url):
http_client=AsyncHTTPClient()
response=yield http_client.fetch(url)
#raise gen.Return(response.body)
return response.body
Python 3.5 :async 和 await
Python 3.5中引进了async和await关键字(使用这些关键字也被称为本地协程).在Tornado4.3中,你可以代替使用yield的协程来使用它们.简单的使用async def foo()来代替使用@gen.coroutine装饰器的函数定义,使用await来代替yield.这篇文档的其他部分使用yield风格来保持和旧版本Python的兼容性,但是async和await将运行迅速.
async def fetch_coroutine(url):
http_client=AsyncHTTPClient()
response=await http_client.fetch(url)
return response.body
await关键字没有yield关键字功能多.例如,在基于yield的协程你可以生成Futures列表,但是在本地协程中你必须在tornado.gen.multi中包装列表.你可以使用tornado.gen.conver_yielded来将yield返回的结果转换为await支持返回的形式.
由于本地协程对没有显式的绑定到特别的框架(比如:它们没有使用像tornado.gen.coroutine或asyncio.coroutine的装饰器),不是所有的都彼此兼容.当第一次调用协程时有一个coroutine runner,然后直接使用await调用的所有协程分享.Tornado coroutine runner设计为多功能的,从任何框架中接受awaitable对象;别的coroutine runner可能受限制(比如,asyncio coroutine runner不接受其他框架的协程).所以,推荐使用Tornado coroutine runner.从已经使用asyncio runner的协程中调用Tornado runner的协程,使用tornado.platform.asyncio.to_asyncio_future 适配器.
工作原理
包含yield关键字的函数是一个生成器.所有的生成器都是异步的:当调用这个函数时返回一个生成器对象而不是运行直接结束.@gen.coroutine装饰器通过yield表达式与生成器通信,通过返回Future来调用协程.
下面是协程装饰器内部循环的简单版本:
def run(self):
future=self.gen.send(self.next)
def callback(f):
self.next=f.result()
self.run()
future.add_done_callback(callback)
装饰器接受来自生成器的Future,等待(非阻塞)Future的完成,然后拆开Future对象发送结果到生成器作为yield表达式的结果.大多数异步代码从来不直接Future类,而是立即将异步函数返回的Future传递到yield表达式.
如何调用协程
协程不以正常的方式抛出异常;任何抛出的异常将在生成时在Future中捕获.意味着必须以正确的方式来调用协程,否则你将遇到不会注意的错误:
@gen.coroutine
def divide(x,y):
return x/y
def bad_call():
#本应该抛出ZeroDivisionError,因为协程没有正确调用所以它并没有抛出
divide(1,0)
在几乎所有情况下,调用协程的函数本身自己也应该为协程,并且在调用时使用yield关键字.当你在父类 中重写方法时,查阅文档来看是否允许协程使用(文档应该说此方法"可以为协程"或"可以返回Future"):
@gen.coroutine
def good_call():
#yield将拆开divide()返回的Future,然后抛出意外.
yield divide(1,0)
有时候你想要不等待结果直接’触发和忘记’一个协程.这种情况下推荐使用IOLoop.spawn_callback,用IOLoop来负责调用.如果调用失败,IOLoop将记录栈跟踪.
# IOLoop将捕获错误意外,然后在日志中打印出栈跟踪.注意这不像普通的调用方式,因为我们通过IOLoop来调用函数对象.
IOLoop.current().spawn_callback(divide,1,0)
最后,在高级别的程序中,如果IOLoop没有运行,你可以启动IOLoop来运行协程,然后通过IOLoop.run_sync()方法来停止IOLoop.
这经常用于在面向批操作的程序中启动main函数:
#run_sync()不接收参数,所以你必须在lambda表达式中包装调用
IOLoop.current().run_sync(lambda :divide(1,0))
协程模式
与回调交互
与使用回调而不是Future的异步代码交互,在Task中包装调用.这将为你添加回调参数,然后返回你可以yield的Future.
@gen.coroutine
def call_task():
#注意在some_function上没有括号
#这将被Task转化为
#some_function(other_args,callback=callback)
yield gen.Task(some_function,other_args)
调用阻塞函数
最简单从协程中调用阻塞函数的方式是使用ThreadPoolExecutor,返回与协程兼容的Futures.
thread_pool=ThreadPoolExecutor(4)
@gen.coroutine
def call_blocking():
yield thread_pool.submit(blocking_func,args)
并行
协程装饰器识别值为Futures的列表和字典,然后并行等待所有的Futures.
@gen.coroutine
def parallel_fetch(url1,url2):
resp1,resp2=yield [http_client.fetch(url1),http_client.fetch(url2)]
@gen.coroutine
def parallel_fetch_many(urls):
responses=yield [http_client.fetch(url) for url in urls]
#responses 是同样顺序的HTTPResponse列表
@gen.coroutine
def parallel_fetch_dict(urls):
responses=yield {url:http_client.fetch(url) for url in urls}
#responses 是一个字典{url:HTTPResponse}
交错
有时候将Future保存起来而不是立即迭代它更有用,所以你可以在等待前进行另一操作:
@gen.coroutine
def get(self):
fetch_future=self.fetch_next_chunk()
while True:
chunk=yield fetch_future
if chunk is None:break
self.write(chunk)
fetch_future=self.fetch_next_chunk()
yield self.flush()
循环
因为Python中没有对for或while迭代进行yield并且捕获yield的结果.所以对于协程的循环有些微妙.你需要将循环条件和获得结果分离开来,下面是使用Motor的例子:
import motor
db=motor.MotorClient().test
@gen.coroutine
def loop_example(collection):
cursor=db.collection.find()
while(yield cursor.fetch_next):
doc=cursor.next_object()
在后台运行
PeriodicCallback在协程中不被正常使用,代替的是协程可以包含while True:循环然后使用tornado.gen.sleep:
@gen.coroutine
def minute_loop():
while True:
yield do_something()
yield gen.sleep(60)
#协程用spawn_callback()开始永久循环
IOLoop.current().spawn_callback(minute_loop)
有时候需要更复杂的循环.例如,上一个循环执行了60+N秒,N指do_something()的运行时间.为了精确的在每60秒中执行,使用上面的交错模式:
@gen.coroutine
def minute_loop2():
while True:
nxt=gen.sleep(60) #开启计时器
yield do_something() #计时器时间间隔到时运行
yield nxt #等待计时器下一个间隔