skip to Main Content
你的Python高效学习之道 025-5987-6503 contact@lessoncode.com

Python的Asyncio

原始的

asyncio应该在协同程序的帮助下实现异步IO。最初作为一个图书馆围绕产量和产量从表达式实现,它现在是一个更复杂的野兽,随着语言的发展同时。所以这里是你需要知道的当前事物集:

  • 事件循环
  • 事件循环策略
  • awaitables
  •  协同功能
  •  老式协同功能
  • 协同程序
  • 协同包装
  • 发电机
  •  期货
  • 并发期货
  • 任务
  • 手柄
  • 遗嘱执行人
  • 运输
  • 协议

此外,语言获得了一些新的特殊方法:

  • __aenter__和__aexit__与块异步
  • __aiter__和__anext__用于异步迭代器(异步循环和异步解析)。为了额外的乐趣,协议已经改变了一次。在3.5版本中,它返回了一个等待(3.6)的等待时间,它将返回一个新的异步发生器。
  •  __await__为定制等待

这有点知道,文档涵盖了这些部分。不过这里有一些笔记,我对这些事情做了更好的了解:

事件循环

asyncio中的事件循环与第一次看到的有点不同。在表面上,每个线程看起来都有一个事件循环,但这不是真正的工作原理。这是我认为这是如何工作的:

 

  •  如果你是主线程,当你调用asyncio.get_event_loop()时创建一个事件循环,
  • 如果您是任何其他线程,则会从asyncio.get_event_loop()引发运行时错误,
  • 您可以随时将asyncio.set_event_loop()绑定到当前线程的事件循环。可以使用asyncio.new_event_loop()函数创建这样的事件循环。
  • 可以使用事件循环而不绑定到当前线程。
  • asyncio.get_event_loop()返回线程绑定事件循环,它不返回当前运行的事件循环。

由于几个原因,这些行为的组合是超级混乱的。首先,您需要知道这些功能是全局设置的底层事件循环策略的委托。默认情况是将事件循环绑定到线程。或者,理论上可以将事件循环绑定到绿叶或类似的东西,如果愿意的话。然而,重要的是要知道库代码不会控制策略,因此不能使asyncio适用于一个线程。

其次,asyncio不需要通过策略将事件循环绑定到上下文。一个事件循环可以正常工作。然而,这是库代码作为协同程序的第一个问题,或类似的东西不知道哪个事件循环负责调度它。这意味着如果从协调程序中调用asyncio.get_event_loop(),那么您可能无法获得运行您的事件循环。这也是所有API采用可选的显式循环参数的原因。所以例如找出当前正在运行的协同程序,不能调用这样的东西:

def get_task():
loop = asyncio.get_event_loop()
try:
return asyncio.Task.get_current(loop)
except RuntimeError:
return None

相反,循环必须明确地传递。这进一步要求您在库代码中显式地遍历循环,否则将会发生非常奇怪的事情。不知道该设计的想法是什么,但是如果这不是固定的(例如get_event_loop()返回实际运行的循环),则唯一有意义的其他更改是明确禁止显式循环传递并要求它被绑定到当前上下文(线程等)。

由于事件循环策略不为当前上下文提供标识符,因此库也不可能以任何方式“键入”到当前上下文。也没有任何回调,可以勾勒出这样一个上下文的撕裂,这进一步限制了现实可行。
等待和协调

在我谦虚的意见中,Python最大的设计错误是如此重载迭代器。它们现在不仅用于迭代,还用于各种类型的协同程序。 Python中迭代器最大的设计错误之一是StopIteration如果没有被捕获,则会发生冒泡。这可能会导致非常令人沮丧的问题,其中某个异常可能会导致其他地方的发电机或协同程序中止。这是一个长期的问题,例如金贾必须打架。模板引擎内部呈现为一个生成器,当一个模板由于某种原因引起了一个StopIteration时,渲染刚刚结束。

Python正在慢慢学习更多的超载这个系统的教训。首先在3.something的asyncio模块着陆,没有语言支持。所以它是装饰和发电机一路下来。为了实现从支持和更多的收益,StopIteration再一次超载。这导致了令人惊讶的行为:

>> def foo(n):
… if n in (0, 1):
… return [1] … for item in range(n):
… yield item * 2

>>> list(foo(0))
[] >>> list(foo(1))
[] >>> list(foo(2))
[0, 2]

没有错误,没有警告。只是不是你期望的行为。这是因为来自作为生成器的函数的值的返回值实际上引发了一个没有被迭代器协议接收但仅在协同代码中处理的单个arg的StopIteration。

随着3.5和3.6的改变很多,因为现在除了生成器之外,我们还有协同程序对象。而不是通过包装生成器来形成协同程序,而不是直接创建协同程序的单独对象。它通过使用异步函数前缀来实现。例如,async def x()将使这样的协程。现在在3.6中会有独立的异步发生器,它会引发AsyncStopIteration来保持分离。此外,Python 3.5及更高版本现在将会导入(generator_stop),如果代码在迭代步骤中引发了StopIteration,则会引发RuntimeError。

为什么我提到这一切?因为旧的东西并没有真的消失。发电机仍然有发送和转发,仍然主要表现为发电机。这是你需要知道的很多东西,在很长一段时间内。

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

  • 等待:一个__await__方法的对象。这例如由本地协同程序和旧式协调程序等一些实现。
  • coroutinefunction:返回一个本地协同程序的函数。不要与返回协程的函数混淆。
  • 协作:协调一个协议。请注意,根据我现在的说明,旧的异步协同程序不被视为协同程序。至少inspect.iscoroutine不认为一个协程。然而,它被未来/等待分支所接受。

特别令人困惑的是,asyncio.iscoroutine函数和inspect.iscoroutine函数正在做不同的事情。与inspect.iscoroutine相同,并检查。请注意,即使检查在类型检查中也不知道asycnio传统协议函数,当您检查等待状态时,即使不符合__await__也是明显的。
协同程序包装

每当你运行async def Python调用线程本地协同程序包装器。它是用sys.set_coroutine_wrapper设置的,它是一个可以包装的功能。看起来有点像这样:

>> import sys
>>> sys.set_coroutine_wrapper(lambda x: 42)
>>> async def foo():
… pass

>>> foo()
__main__:1: RuntimeWarning: coroutine ‘foo’ was never awaited
42

在这种情况下,我从来没有实际调用原来的功能,只是给你一个可以做的事情。据我所知,这是总是线程本地的,所以如果你换掉事件循环策略,你需要单独弄清楚如何使这个协同程序包装器与同样的上下文同步,如果你想做的事情。生成的新线程将不会从父线程继承该标志。

这不能与asyncio协程程序包装代码混淆。

等待和期货

有些事情是等待的。据我看到,以下事情被认为是等待的:

  • 本地协同程序
  • 发生器设置了假CO_ITERABLE_COROUTINE标志(我们将覆盖)
  • 具有__await__方法的对象

基本上这些都是__await__方法的所有对象,除了发电机不是出于传统原因。 CO_ITERABLE_COROUTINE标志来自哪里?它来自一个协同程序包装(现在要与sys.set_coroutine_wrapper混淆),这是@ asyncio.coroutine。通过一些间接方法,将使用types.coroutine(要与types.CoroutineType或asyncio.coroutine混淆)来包装生成器,这将重新创建带有附加标志CO_ITERABLE_COROUTINE的内部代码对象。

那么现在我们知道这些东西是什么,什么是期货?首先,我们需要清除一件事:在Python 3中实际上有两种(完全不兼容)的期货类型。asyncio.futures.Future和concurrent.futures.Future。一个来到另一个之前,但是他们也都在甚至在异步中使用。例如,asyncio.run_coroutine_threadsafe()将派生协同程序到在另一个线程中运行的事件循环,但它将返回一个concurrent.futures.Future对象,而不是asyncio.futures.Future对象。这是有道理的,因为只有concurrent.futures.Future对象是线程安全的。

所以现在我们知道有两个不兼容的期货,我们应该澄清什么期货是在同步的。老实说,我不完全确定差异在哪里,但我现在称之为“最终”。这是一个对象,最终将持有一个价值,你可以做一些处理与最终的结果,而它仍在计算。一些这样的变化被称为延期,其他的被称为承诺。有什么确切的区别在于我的头脑。

你将来能做些什么?您可以附加一个回调,一旦它准备就绪,将被调用,或者你可以附加一个将在以后失败时被调用的回调。此外,您可以等待它(它实现__await__并因此等待)。另外期货可以取消。

那么你如何得到这样的未来?通过在等待对象上调用asyncio.ensure_future。这也将使一个好的旧发电机成为这样的未来。但是,如果您阅读文档,您将阅读该asyncio.ensure_future实际返回一个任务。那是什么工作呢?

任务

任务是一个包装协同工作的未来。它的作用就像一个未来,但它也有一些额外的方法来提取包含的协程的当前堆栈。我们已经看到前面提到的任务,因为它是通过Task.get_current找出当前正在执行的事件循环的主要方法。

在任务和期货取消方式方面也有差异,但这超出了这个范围。取消是自己的整个野兽。如果你在一个协会,并且你知道你正在运行,你可以通过Task.get_current获得你自己的任务,但是这需要知道你调度什么事件循环,这可能是或可能不是线程绑定的。

协调程序不可能知道哪个循环与它一起。此外,该任务还没有通过公共API提供该信息。但是,如果您设法抓住任务,则可以当前访问task._loop以查找事件循环。

手柄

除了所有这些都有句柄。句柄是不能等待的待处理执行的不透明对象,但可以取消它们。特别是如果您使用call_soon或call_soon_threadsafe(和其他一些)调度执行调用,那么您可以使用该方法来尽可能地取消执行,但是您无法等待呼叫的实际发生。

执行人

既然你可以有多个事件循环,但是不太明显的是,每个线程使用多于一个这样的事情是一个明显的假设,一个常见的设置是让每个线程都有一个事件循环。那么你如何通知另一个事件循环来做一些工作呢?您不能在另一个线程的事件循环中调度回调,并返回结果。为此,您需要使用执行器。

执行者来自concurrent.futures,例如,它们允许您将工作安排到本身未被发现的线程中。例如,如果在事件循环中使用run_in_executor来调度在另一个线程中调用的函数。然后,结果返回为asyncio协同程序,而不是像run_coroutine_threadsafe那样的并发协同程序。我还没有足够的心理能力来弄清楚为什么这些API存在,你应该如何使用以及何时使用。该文档表明,执行程序的东西可以用来构建多处理的东西。

运输和议定书

我总是认为这是令人困惑的事情,但这基本上是Twisted中相同概念的逐字复制。

如何使用asyncio

现在我们知道大致了解asyncio我发现了一些人们在写asyncio代码时似乎使用的模式:

  • 将事件循环传递给所有协同程序。这似乎是社区的一部分。给出一个关于什么循环要安排的协同学习知识,使协调者能够了解它的任务。
  • 或者你要求循环绑定到线程。这也让协会了解到这一点。理想地支持两者。可悲的是,社区已经被撕毁了。
  • 如果你想使用上下文数据(认为线程本地人),你现在有点不幸。最流行的解决方法显然是atlassian的aiolocals,它基本上需要您手动将上下文信息传播到协同程序中,因为解释器不提供支持。这意味着如果您有一个实用程序库产生协同程序,您将丢失上下文。
  • 忽略Python中的旧协同工具存在。仅使用3.5新的async def关键字和co。特别是,您将需要这样做有点享受体验,因为使用较旧的版本,您没有异步上下文管理器,这对于资源管理来说是非常必要的。
  • 学习重新启动事件循环进行清理。这是我需要的时间比我想要的更长的时间,但处理使用异步代码编写的清理逻辑的最简单的方法是重新启动事件循环几次,直到没有待处理。由于可悲的是,没有一个共同的模式来处理这个问题,你最终会遇到一些丑陋的解决方法。例如,aiohttp的Web支持也执行此模式,所以如果要组合两个清理逻辑,您可能必须重新实现它提供的实用程序帮助程序,因为该帮助器在完成后完全清除循环。这也不是我看到的第一个图书:
  • 使用子进程是不明显的。您需要在主线程中运行一个事件循环,我假设它正在监听信号事件,然后将其分派到其他事件循环。这要求循环通过asyncio.get_child_watcher()。attach_loop(…)通知。
  • 编写支持异步和同步的代码有点像是丢失的原因。当您开始聪明并尝试在同一对象上支持和异步时,它也会很快变得危险。
  • 如果你想给一个协会一个更好的名字,以弄清楚为什么它不被等待,设置__name__没有帮助。您需要设置__qualname__,而不是打印机使用的错误消息。
  • 有时内部类型的对话可能会使你失望。特别是,asyncio.wait()函数将确保所有传递的内容都是未来的,这意味着如果您传递协同程序,那么您将很难找出您的协程完成或正在挂起,因为输入对象不再匹配输出对象在这种情况下,唯一真正的事情是确保一切都是未来的前提。

上下文数据

除了我对如何最好地编写API的麻烦的复杂性和缺乏理解之外,我最大的问题是完全缺乏上下文本地数据的考虑。这是节点社区现在学到的东西。存在连续本地存储,但已被接受为实施时间太晚。经常使用持续的本地存储和类似概念在并发环境中实施安全策略,并且该信息的损坏可能导致严重的安全问题。

事实上,Python甚至没有任何商店,这是更令人失望的。我正在研究这个,特别是因为我正在调查如何最好地支持Sentry的面包屑的asyncio,我没有看到一个合理的方式来做到这一点。在asyncio中没有上下文的概念,没有办法找出您正在使用的通用代码中的哪个事件循环,并且没有对所有这些信息进行监视,此信息将不可用。

节点目前正在为此问题寻找长期解决方案。这是不可忽视的事情,可以看出,这是所有生态系统中经常性的问题。它提出了JavaScript,Python和.NET环境。这个问题被命名为异步上下文传播,解决方案有许多名称。在Go中,需要使用上下文包并将其显式传递给所有goroutine(不是一个完美的解决方案,而是至少一个)。 .NET具有本地调用上下文形式的最佳解决方案。它可以是线程上下文,Web请求上下文或类似的东西,但是它会自动传播,除非被禁止。这是什么目标的黄金标准。自从15年以来,微软已经解决了这个问题。

我不知道生态系统是否仍然足够年轻,可以添加逻辑调用上下文,但现在还可能是时间。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Back To Top