Flask 作者写万字长文谈 asyncio(上)

Posted 编程派

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flask 作者写万字长文谈 asyncio(上)相关的知识,希望对你有一定的参考价值。

译者: firstadream 

原文:

全文约 9499 字,读完可能需要 14 分钟。

最近我开始发力钻研 Python 的新 模块。原因是我需要做一些事情,使用事件 IO 会使这些事情工作得更好,炙手可热的 asynio 正好可以用来牛刀小试。从试用的经历来看,该模块比我预想的复杂许多,我现在可以非常肯定地说,我不知道该如何恰当地使用 asyncio。

从 Twisted 框架借鉴一些经验来理解 asynio 并非难事,但是,asyncio 包含众多的元素,我开始动摇,不知道如何将这些孤立的零碎拼图组合成一副完整的图画。我已没有足够的智力提出任何更好的建议,在这里,只想分享我的困惑,求大神指点。

原语

asyncio 通过协程coroutines 的帮助来实现异步 IO。最初它是通过 yield 和 yield from 表达式实现的一个库,因为 Python 语言本身演进的缘故,现在它已经变成一个更复杂的怪兽。所以,为了在同一个频道讨论下去,你需要了解如下一些术语:

  • 事件循环

  • 事件循环策略

  • awaitable

  • 协程函数

  • 老式协程函数

  • 协程

  • 协程封装

  • 生成器(generator)

  • future

  • 并发的future

  • 任务(task)

  • 句柄

  • 执行器(executor)

  • 传输(transport)

  • 协议

此外,Python 还新增了一些新的特殊方法:

  • __aenter__ 和 __aenter__,用于异步块操作

  • __aiter__ 和 __anext__,用于异步迭代器(异步循环和异步推导)。为了更强大些,协议已经改变过一次了。 在 Python 3.5 它返回一个 awaitable(这是个协程);在 3.6它返回一个新的异步生成器。

  • __await__,用于自定义的 awaitable

你还需要了解相当多的内容,文档涵盖了那些部分。尽管如此,我做了一些额外说明以便对其有更好的理解:

事件循环

asyncio 事件循环和你第一眼看上去的略有不同。表面看,每个线程都有一个事件循环,然而事实并非如此。我认为它们应该按照如下的方式工作:

  • 如果是主线程,当调用 asyncio.get_event_loop() 时创建一个事件循环。

  • 如果是其它线程,当调用 asyncio.get_event_loop() 时返回运行时错误。

  • 当前线程可以使用 asyncio.set_event_loop() 在任何时间节点绑定事件循环。该事件循环可由 asyncio.new_evet_loop() 函数创建。

  • 事件循环可以在不绑定到当前线程的情况下使用。

  • asyncio.get_event_loop() 返回绑定线程的事件循环,而非当前运行的事件循环。

这些行为的组合是超混淆的,主要有以下几个原因。 首先,你需要知道这些函数被委托到全局设置的底层事件循环策略。 默认是将事件循环绑定到线程。 或者,如果需要的话,可以在理论上将事件循环绑定到一个 greenlet 或类似的。 然而,重要的是要知道库代码不控制策略,因此不能推断 asyncio 将适用于线程。

其次,asyncio 不需要通过策略将事件循环绑定到上下文。 事件循环可以单独工作。 但是这正是库代码的第一个问题,因为协同程序或类似的东西并不知道哪个事件循环负责调度它。 这意味着,如果从协程中调用 asyncio.get_event_loop(),你可能没有机会取得事件循环。 这也是所有 API 均采用可选的显式事件循环参数的原因。 举例来说,要弄清楚当前哪个协程正在运行,不能使用如下调用:

 
   
   
 
  1.    def get_task():

  2.        loop = asyncio.get_event_loop()

  3.        try:

  4.            return asyncio.Task.get_current(loop)

  5.        except RuntimeError:

  6.            return None

相反,必须显式地传递事件循环。这进一步要求你在库代码中显式地遍历事件循环,否则可能发生很奇怪的事情。

我不知道这种设计的思想是什么,但如果不解决这个问题(例如 get_event_loop() 返回实际运行的事件循环),那么唯一有意义的其它方案是明确禁止显式事件循环传递,并要求它绑定到当前上下文(线程等)。

由于事件循环策略不提供当前上下文的标识符,因此库也不可能以任何方式"索引"到当前上下文。 也没有回调函数用来监视这样的上下文的拆除,这进一步限制了实际可以开展的操作。

awaitable 与协程coroutine

以我的愚见,Python 最大的设计错误是过度重载迭代器。它们现在不仅用于迭代,而且用于各种类型的协程。 Python 中迭代器最大的设计错误之一是如果 StopIteration 没有被捕获形成的空泡。

这可能导致非常令人沮丧的问题,其中某处的异常可能导致其它地方的生成器或协同程序中止。 这是一个长期存在的问题,基于 Python 的模板引擎如 Jinja 经常面临这种问题。

该模板引擎在内部渲染为生成器,并且当由于某种原因的模板引起 StopIteration 时,渲染就停止在那里。

Python 慢慢认识到了过度重载的教训。首先在 3.x 版本加入 asyncio 模块,并没有语言级支持。 所以自始至终它不过仅仅是装饰器和生成器而已。

为了实现 yield from 以及其它东西, StopIteration 再次重载。这导致了令人困惑的行为,像这样:

 
   
   
 
  1. >>> def foo(n):

  2. ...  if n in (0, 1):

  3. ...   return [1]

  4. ...  for item in range(n):

  5. ...   yield item * 2

  6. ...

  7. >>> list(foo(0))

  8. []

  9. >>> list(foo(1))

  10. []

  11. >>> list(foo(2))

  12. [0, 2]

没有错误,没有警告。只是不是你所期望的行为。 这是因为从一个作为生成器的函数中 return的值实际上引发了一个带有单个参数的 StopIteration,它不是由迭代器协议捕获的,而只是在协程代码中处理。

在 3.5 和 3.6 有很多改变,因为现在除了生成器我们还有协程对象。除了通过封装生成器来生成协程,没有其它可以直接生成协程的单独对象。它是通过用给函数加 async 前缀来实现。 例如 async def x() 会产生这样的协程。 现在在 3.6,将有单独的异步生成器,它通过触发 AsyncStopIteration 保持其独立性。

此外,对于Python 3.5 和更高版本,导入新的 future 对象( generator_stop),如果代码在迭代步骤中触发 StopIteration,它将引发 RuntimeError

为什么我提到这一切? 因为老的实现方式并未真的消失。 生成器仍然具有 send 和 throw 方法以及协程仍然在很大程度上表现为生成器。你需要知道这些东西,它们将在未来伴随你相当长的时间。

为了统一很多这样的重复,现在我们在 Python 中有更多的概念了:

  • awaitable:具有 __await__方法的对象。 由本地协同程序和旧式协同程序以及一些其它程序实现。

  • 协程函数(coroutinefunction):返回原生协程的函数。 不要与返回协程的函数混淆。

  • 协程(coroutine): 原生的协程程序。 注意,目前为止,当前文档不认为老式 asyncio 协程是协程程序。 至少 inspect.iscoroutine 不认为它是协程。 尽管它被 future/awaitable 分支接纳。

特别令人困惑的是 asyncio.iscoroutinefunction 和 inspect.iscoroutinefunction 正在做不同的事情,这与 inspect.iscoroutine 和 inspect.iscoroutinefunction 情况相同。

值得注意的是,尽管 inspect 在类型检查中不知道有关 asycnio 旧式协程函数的任何信息,但是当您检查 awaitable 状态时它显然知道它们,即使它与 **await** 不一致。

协程封装器coroutine wrapper

每当你运行 async def ,Python 就会调用一个线程局部的协程封装器。它由 sys.set_coroutine_wrapper 设置,并且它是可以包装这些东西的一个函数。

看起来有点像如下代码:

 
   
   
 
  1. >>> import sys

  2. >>> sys.set_coroutine_wrapper(lambda x: 42)

  3. >>> async def foo():

  4. ...  pass

  5. ...

  6. >>> foo()

  7. __main__:1: RuntimeWarning: coroutine 'foo' was never awaited

  8. 42

在这种情况下,我从来没有实际调用原始的函数,只是给你一个提示,说明这个函数可以做什么。

目前我只能说它总是线程局部有效,所以,如果替换事件循环策略,你需要搞清楚如何让协程封装器在相同的上下文同步更新。创建的新线程不会从父线程继承那些标识。

这不要与 asyncio 协程封装代码混淆。

Armin Ronacher

软件开发者和开源骨灰, Flask 框架的创造者。


题图:pexels,CC0 授权。

点击阅读原文,查看更多 Python 教程和资源。

以上是关于Flask 作者写万字长文谈 asyncio(上)的主要内容,如果未能解决你的问题,请参考以下文章

程序员的十年工作创业血泪史,万字长文,仔细读完,受益匪浅

研发效能度量实践者指南(万字长文)

微服务架构技术栈选型手册(万字长文)

❤JavaScript系列6部曲:语法篇(万字长文)❤

《浪潮之巅》作者吴军万字长文:中国算力的危与机

《浪潮之巅》作者吴军万字长文:中国算力的危与机