Tornado---协程

协程

协程是推荐在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             #等待计时器下一个间隔
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值