[Python]前往Python Async -5的旅程。
#网络开发人员 #python #网络

在上一篇文章中,我们终于在异步编程的上下文中弄清楚了(?)。但是,我们只看着单个树而不是森林。我们想尝试通过查看图书馆asyncio来绘制单个异步函数如何处理单个异步函数。

备注:为什么不curiotrio

两者都是非常著名的图书馆,但是有几个原因是我选择 asyncio 而不是本文:

  • 这是一个stl:即,维护良好且期望结构良好。
  • 使用诸如事件循环,未来和任务等概念。由于如今的许多其他语言和框架都使用了类似的(如果不是相同)的概念,我认为首先了解这些概念会更有益,这样我们也可以更熟悉其他语言中的其他用例。

所以,让我们挖掘出Python中 asyncio 提供的异步API。我们要回答的主要问题是:

  1. 这些神秘的术语是什么:事件循环,未来和任务?
  2. 这些概念如何与我们在先前文章中看到的Coroutine概念和谐相处?

但请注意,我只会研究 asyncio 库,并且不会涵盖任何抽象和理论概念。这不仅是因为它们超出了我们的范围,还因为我坚信任何知识都应该从具体和实际事物开始。

备注本文基于Python 3.11中的Asyncio和Unix OS(MacOS)

1.详细说明异步术语

自从我开始学习一般的Web技术以来是。尽管Wikipedia在大量细节上解释了这些概念,但是在大多数情况下,它们充满了其他计算机科学术语,因此您最终不得不理解其他多个术语来掌握原始的单术语。哦,根本不是初学者!

因此,在这里,我们想研究这些概念在 asyncio 库中的实际实现。当然,其他库(不仅是Python中的图书馆)具有自己对这些概念的实施版本,但我相信核心行为必须相似。

事件循环

让我们从事件循环开始。那么什么是事件循环?是的,这只是一种编程模式。但是在这里,我们想知道它是如何工作的,至少在 asyncio

从Python 3.7中,我们使用asyncio.run运行一个Coroutine对象。它是在Python代码的一系列同步执行中的其他 asyncio API的入口。如果您深入到底部,您会看到一个名为 _UnixSelectorEventLoop的课程。这是我们的默认事件循环对象,至少在UNIX OS上。那么,选择器是该类的实例成员?您会发现DefaultSelector,这是determined here在另一个STL select的帮助下。在弄清楚哪个选择器之前,请查看我们在这里有多少种选择器:kqueue,epoll,devpoll等。

# https://github.com/python/cpython/blob/2d037fb406fd8662862c5da40a23033690235f1d/Lib/asyncio/unix_events.py#L57 

class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
    """Unix event loop.

    Adds signal handling and UNIX Domain Socket support to SelectorEventLoop.
    """
# https://github.com/python/cpython/blob/2d037fb406fd8662862c5da40a23033690235f1d/Lib/selectors.py#L609

if _can_use('kqueue'):
    DefaultSelector = KqueueSelector
elif _can_use('epoll'):
    DefaultSelector = EpollSelector
elif _can_use('devpoll'):
    DefaultSelector = DevpollSelector
elif _can_use('poll'):
    DefaultSelector = PollSelector
else:
    DefaultSelector = SelectSelector

选择器是系统调用。因此,我们可以看到,默认情况下,我们的 asyncio 库试图使用这些OS级内核API在内部处理异步I/OS(是否太多夸张了,以至于说该事件循环在异步只是这些I/O系统调用的包装器吗?)。选择器是我们希望我们的活动循环使用的选择。简而言之,他们检查了我们打开的插座中是否有任何传入的事件(有关详细信息,如果您是MacOS/Linux用户,请参阅Kqueue/Epoll的Man页面。其他好的参考文献是beej’s guidethis linux programming bible book, chapter 6311)。通过这些投票API,内核认识到哪些插座可以阅读。

现在,我们知道 asyncio 中的事件循环在引擎盖下进行系统调用(选择器),让我们看看选择器的位置。如果您查看koude2(顺便说一句,这是其他事件环实例方法所基于的核心函数),它将调用this line中的koude3 function并处理就绪事件。在这里,processing events意味着清理已取消的事件并为“就绪事件”设置注册的回调,以最终在_run_once()中执行函数。

# https://github.com/python/cpython/blob/2d037fb406fd8662862c5da40a23033690235f1d/Lib/asyncio/base_events.py#L1845

def _run_once(self):

  # ---- code omitted ---- 

  event_list = self._selector.select(timeout)
  self._process_events(event_list)

  # ---- code omitted ---- 

  ntodo = len(self._ready)
  for i in range(ntodo):
    handle = self._ready.popleft()

    # ---- code omitted ---- 
    handle._run()

  # ---- code omitted ---- 

因此,在简而言之, asyncio 中的任何默认事件循环使用poll/select内核API来检查任何传入事件,并执行已注册的回调。我们的下一个问题是,如何将事件和相关回调注册到当前运行事件循环?

任务和未来

任务:创建时

让我们回到我们的入口koude5。您会看到,尽管asnycio.run接受其参数为coroutine,但最终它会使用事件循环的koude7 function创建一个任务对象,并将其扔到事件循环中,默认情况下,该对象会生成一个实例Task类。然后,此任务对象注册关闭事件循环的回调koude9event loop to which it is connected(请注意,Task类是Future类的子类)。最后,事件循环开始运行。

# https://github.com/python/cpython/blob/2d037fb406fd8662862c5da40a23033690235f1d/Lib/asyncio/runners.py#L86

def run(self, coro, *, context=None):
  # ---- code omitted ----

  task = self._loop.create_task(coro, context=context)

  # ---- code omitted ----

  try:
    return self._loop.run_until_complete(task)

  # ---- code omitted ----


def run_until_complete(self, future):
  # ---- code omitted ----

  future.add_done_callback(_run_until_complete_cb)
  try:
    self.run_forever()

  # ---- code omitted ----

但是发生了什么?事件循环在_run_once()中运行即将运行的回调,所以我们的任务是为了完成循环而创建的,它是从什么都不做的?这是我们所缺少的:by the time it is created, the task registers a callback called koude13 with the current event loop

任务:__STEP()和__ -Wakeup()

实际上是我们使用的Task类的接口,运行注册的Coroutine的逻辑,并且有两个控制Coroutine的函数:koude13koude16(这些dunders(这些dunders)(大概是为了防止出乎意料的意外)继承和滥用这些功能)。

但是,__step()是所有魔术开始的地方,而__wakeup()是使__step()继续前进的原因。它使用send(None)启动了任务的Coroutine。当IT gets a future object at the bottom of the coroutine chain时,任务等待那个低级的未来,通过注册__wakeup()回调来完成其工作和requests the event loop to wake it up。此__wakeup()反过来触发了__step(),我们将在这两个功能之间有某种曲折的运动,直到任务为completed with the koude24 exception

# https://github.com/python/cpython/blob/2d037fb406fd8662862c5da40a23033690235f1d/Lib/asyncio/tasks.py#L250

def __step(self, exc=None):
  # ---- code omitted ----
  try:
    # ---- code omitted ----
    result = coro.send(None)

  except StopIteration as exc:
    # ---- code omitted ----
    super().set_result(exc.value)

  else:
    # ---- code omitted ----
    result.add_done_callback(.__wakeup, context=self._context)

    # ---- code omitted ----

def __wakeup(self, future):
  # ---- code omitted ----

  try:
    future.result()

  # ---- code omitted ----

  else:
    self.__step()

  # ---- code omitted ----      

那么,无论如何,任务是什么?总而言之,它是一个对象,可以通过与当前事件循环进行通信来控制要连续执行的coroutine。类似地,coroutine是功能的主体,而其任务是继续单击调试器的下一个按钮的人,以便以顺序执行功能。

Image description

未来:什么是未来的对象?

您可能已经注意到,我没有提到对未来对象的任何定义,而只关注任务对象如何与其coroutine一起工作,即使Task类从未来类中继承。

那么,什么是未来的对象?如果您一目了然

  • state实例变量:未来对象的当前状态
  • resultexception实例变量:这个未来对象的最终结果,set_result()set_exception()
  • add_done_callback()remove_done_callback():在事件循环中注册回调
  • __await__:必须在Coroutine内部调用的魔术函数

因此,将来的对象只是一个界面,旨在与特定的运行事件循环进行交互,并且程序员可以像Coroutine一样选择使用它。可以描述为:

  • 一个可以在给定时间表示特定状态的对象
  • 一旦其状态为_FINISHED,我们可以期望它具有结果或错误
  • 我们可以等待它提供完成的结果或错误(在Coroutine内部)。
  • 我们可以通过其事件循环直接注册所有回调(您可能会注意到,这些功能与JS中的Promise相似,但是虽然async在JS return Promises中的函数,而Python async函数返回返回的coroutines,这些coroutines不是未来的。因此,在Python中,我们在实际执行逻辑(函数)和事件循环之间具有更复杂的层。

official documentation描述了未来是为了封装异步I/OS的低级别,由程序员决定如何使用它。但是为什么它提到低水平?

我想那是因为未来是我们从中获得所需的实际结果值的对象。在Coroutine链的最低级别上,Coroutine应该等待不是Coroutine的东西(当然,我们可以简单地实施该链的最后一个Coroutine,以返回一个没有任何东西的价值使用外部网络,但是这样的Coroutine的意义是什么?)。魔术发生在koude33 of future。这是我们的老朋友yield打招呼的地方,对所有这些async … await …句法糖进行了神秘面纱。因此,最后,所有异步API基本上用于等待未来的对象来提供预期的结果。

使用未来对象的一个​​很好的例子是函数sock_recv。它没有等待另一个Coroutine,而是直接创造了未来,并将其关联为插座文件描述符(这是一个真正的低级别)。在事件循环中注册的回调与选择器API直接相关。

2.大局

我们已经讨论了许多紧密合作的异步对象:Coroutines,任务,期货和事件循环。然而,可能的工作流程的结构非常复杂,因此我们想在异步上下文中绘制工作流程如何发生的全局。

  • 所有事件围绕单个事件循环:在单个线程上运行,事件循环不断使用select/poll apis检查外部I/OS,并执行由任务和其他未来对象注册的回调。

Image description

  • 使用事件循环作为发动机,任务需要运行运行在单个Coroutine中包裹的异步作品的逻辑。任务寄回了带有事件循环的回调,以便这些回调触发coroutine直到完成。

Image description

  • 多亏了类似于yield(最终与yield相关的Coroutines结构),一系列异步执行可以作为单个工作单位组装,避免使用所谓的回调地狱。在Coroutine链的结束时,我们将遇到未来的对象,这些对象实际上给我们带来了预期的结果(或不幸的例外)。

Image description

请注意,我们没有关于 asyncio 库的讨论。我们没有提到诸如“ context”,“ schepend”或“信号”之类的概念,这些概念也是库的重要组成部分。我们之所以忽略他们的原因是,因为我们想知道与执行coroutine函数直接相关的对象在 asyncio 库中,以及它们如何一起工作,而不是研究一个的每个细节可能的工作流程。但是,我希望我也可以重新审视其余的概念,并在不久的将来充分描述整个图片。

结论

感谢您在这段漫长而漫长的旅程(双关语)上度过的时间。从深入研究生成器的CPYTHON源代码到探索Asyncio库,我们涵盖了相当数量的概念和实际代码。让我在这里简要列出它们:

  • 发电机:迭代器,屈服,从简单的Coroutines
  • coroutines:本地统治,等待
  • asyncio :事件循环,任务,未来

配备了这些艰苦的知识,我们现在可以充满信心地使用async... await...语法。我们涵盖的结构和概念不仅仅是Python。他们还将对其他编程语言和框架实现的异步功能提供深入的理解。