Tornado 4.3 文档翻译: 用户指南-协程

译者说

Tornado 4.3于2015年11月6日发布,该版本正式支持Python3.5async/await关键字,并且用旧版本CPython编译Tornado同样可以使用这两个关键字,这无疑是一种进步。其次,这是最后一个支持Python2.6Python3.2的版本了,在后续的版本了会移除对它们的兼容。现在网络上还没有Tornado4.3的中文文档,所以为了让更多的朋友能接触并学习到它,我开始了这个翻译项目,希望感兴趣的小伙伴可以一起参与翻译,项目地址是tornado-zh on Github,翻译好的文档在Read the Docs上直接可以看到。欢迎Issues or PR。

协程

Tornado中推荐使用协程写异步代码. 协程使用了Python的yield关键字代替链式回调来将程序挂起和恢复执行(像在 gevent中出现的轻量级线程合作方式有时也被称为协程,但是在Tornado中所有的协程使用明确的上下文切换,并被称为异步函数).

使用协程几乎像写同步代码一样简单, 并且不需要浪费额外的线程. 它们还通过减少上下文切换来 使并发编程更简单.

例子:

    from tornado import gen

    @gen.coroutine
    def fetch_coroutine(url):
        http_client = AsyncHTTPClient()
        response = yield http_client.fetch(url)
        # 在Python 3.3之前, 在generator中是不允许有返回值的
        # 必须通过抛出异常来代替.
        # 就像 raise gen.Return(response.body).
        return response.body

Python 3.5:asyncawait

Python 3.5 引入了asyncawait关键字(使用这些关键字的函数也被称为"原生协程"). 从Tornado 4.3,你可以用它们代替yield为基础的协程.只需要简单的使用async def foo()在函数定义的时候代替@gen.coroutine装饰器, 用await代替yield. 本文档的其他部分会继续使用yield的风格来和旧版本的Python兼容, 但是如果asyncawait可用的话,它们运行起来会更快:


    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.convert_yielded来把任何使用yield工作的代码转换成使用await的形式.

虽然原生协程没有明显依赖于特定框架(例如它们没有使用装饰器,例如tornado.gen.coroutineasyncio.coroutine), 不是所有的协程都和其他的兼容. 有一个coroutine runner在第一个协程被调用的时候进行选择, 然后被所有用await直接调用的协程共享. Tornado的协程执行者(coroutine runner)在设计上是多用途的,可以接受任何来自其他框架的awaitable对象;其他的协程运行时可能有很多限制(例如,asyncio协程执行者不接受来自其他框架的协程).基于这些原因,我们推荐组合了多个框架的应用都使用Tornado的协程执行者来进行协程调度.为了能使用Tornado来调度执行asyncio的协程, 可以使用tornado.platform.asyncio.to_asyncio_future适配器.

它是如何工作的

包含了yield关键字的函数是一个生成器(generator). 所有的生成器都是异步的; 当调用它们的时候,会返回一个生成器对象,而不是一个执行完的结果.@gen.coroutine装饰器通过yield表达式和生成器进行交流, 而且通过返回一个.Future与协程的调用方进行交互.

下面是一个协程装饰器内部循环的简单版本:

    # tornado.gen.Runner 简化的内部循环
    def run(self):
        # send(x) makes the current yield return x.
        # It returns when the next yield is reached
        future = self.gen.send(self.next)
        def callback(f):
            self.next = f.result()
            self.run()
        future.add_done_callback(callback)

装饰器从生成器接收一个.Future对象, 等待(非阻塞的)这个.Future对象执行完成, 然后"解开(unwraps)"这个.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))

协程模式

结合 callback

为了使用回调代替.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)

调用阻塞函数

从协程调用阻塞函数最简单的方式是使用concurrent.futures.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]
        # 响应是和HTTPResponses相同顺序的列表

    @gen.coroutine
    def parallel_fetch_dict(urls):
        responses = yield {url: http_client.fetch(url)
                            for url in urls}
        # 响应是一个字典 {url: HTTPResponse}

交叉存取

有时候保存一个 .Future 比立即yield它更有用, 所以你可以在等待之前
执行其他操作:

    @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)

    # Coroutines that loop forever are generally started with
    # spawn_callback().
    IOLoop.current().spawn_callback(minute_loop)

有时可能会遇到一个更复杂的循环. 例如, 上一个循环运行每次花费60+N秒,其中Ndo_something()花费的时间. 为了准确的每60秒运行,使用上面的交叉模式:

    @gen.coroutine
    def minute_loop2():
        while True:
            nxt = gen.sleep(60)   # 开始计时.
            yield do_something()  # 计时后运行.
            yield nxt             # 等待计时结束.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341

推荐阅读更多精彩内容