异步、非阻塞与协程

异步

异步函数会在完成之前返回,在应用中触发下一个动作之前通常会在后 台执行一些工作(和正常的 同步 函数在返回前就执行完所有的事情不同).这里列 举了几种风格的异步接口:

回调参数
返回一个占位符 (Future, Promise, Deferred)
传送给一个队列
回调注册表 (POSIX信号)

不论使用哪种类型的接口, 按照定义 异步函数与它们的调用者都有着不同的交互方 式;也没有什么对调用者透明的方式使得同步函数异步(类似 gevent 使用轻量级线程的系统性能虽然堪比异步系统,但它们并 没有真正的让事情异步).

阻塞

一个函数在等待某些事情的返回值的时候会被 阻塞. 函数被阻塞的原因有很多: 网络I/O,磁盘I/O,互斥锁等.事实上 每个 函数在运行和使用CPU的时候都或多或少 会被阻塞(举个极端的例子来说明为什么对待CPU阻塞要和对待一般阻塞一样的严肃: 比如密码哈希函数 bcrypt, 需要消耗几百毫秒的CPU时间,这已 经远远超过了一般的网络或者磁盘请求时间了).

一个函数可以在某些方面阻塞在另外一些方面不阻塞.例如, tornado.httpclient 在默认的配置下,会在DNS解析上面阻塞,但是在其他网络请 求的时候不阻塞 (为了减轻这种影响,可以用 ThreadedResolver 或者是 通过正确配置 libcurltornado.curl_httpclient 来做). 在Tornado的上下文中,我们一般讨论网络I/O上下文的阻塞,尽管各种阻塞已经被最小 化.

协程

Tornado中推荐使用 协程 写异步代码. 协程使用了Python的 yield 关键字代替链式回调来将程序挂起和恢复执行(像在 gevent 中出现的轻量级线程合作方式有时也被称为协程, 但是在Tornado中所有的协程使用明确的上下文切换,并被称为异步函数).
使用协程几乎像写同步代码一样简单, 并且不需要浪费额外的线程. 它们还通过减少上下文切换来 使并发编程更简单 .
Python 3.5 引入了 asyncawait 关键字(使用这些关键字的 函数也被称为”原生协程”). 从Tornado 4.3, 你可以用它们代替 yield 为基础的协程. 只需要简单的使用 async def foo() 在函数定义的时候代替 @gen.coroutine 装饰器, 用 await 代替yield. 本文档的其他部分会继续使用 yield 的风格来和旧版本的Python兼容, 但是如果 asyncawait 可用的话,它们运行起来会更快:

async and await

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

装饰器从生成器接收一个 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))
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章