在上一篇文章中,我们终于在异步编程的上下文中弄清楚了(?)。但是,我们只看着单个树而不是森林。我们想尝试通过查看图书馆asyncio来绘制单个异步函数如何处理单个异步函数。
。备注:为什么不curio或trio?
两者都是非常著名的图书馆,但是有几个原因是我选择 asyncio 而不是本文:
- 这是一个stl:即,维护良好且期望结构良好。
- 使用诸如事件循环,未来和任务等概念。由于如今的许多其他语言和框架都使用了类似的(如果不是相同)的概念,我认为首先了解这些概念会更有益,这样我们也可以更熟悉其他语言中的其他用例。
所以,让我们挖掘出Python中 asyncio 提供的异步API。我们要回答的主要问题是:
- 这些神秘的术语是什么:事件循环,未来和任务?
- 这些概念如何与我们在先前文章中看到的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 guide和this 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
类。然后,此任务对象注册关闭事件循环的回调koude9到event 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的函数:koude13和koude16(这些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是功能的主体,而其任务是继续单击调试器的下一个按钮的人,以便以顺序执行功能。
未来:什么是未来的对象?
您可能已经注意到,我没有提到对未来对象的任何定义,而只关注任务对象如何与其coroutine一起工作,即使Task
类从未来类中继承。
那么,什么是未来的对象?如果您一目了然
-
state
实例变量:未来对象的当前状态 -
result
和exception
实例变量:这个未来对象的最终结果,set_result()
和set_exception()
-
add_done_callback()
,remove_done_callback()
:在事件循环中注册回调 -
__await__
:必须在Coroutine内部调用的魔术函数
因此,将来的对象只是一个界面,旨在与特定的运行事件循环进行交互,并且程序员可以像Coroutine一样选择使用它。可以描述为:
- 一个可以在给定时间表示特定状态的对象
- 一旦其状态为
_FINISHED
,我们可以期望它具有结果或错误 - 我们可以等待它提供完成的结果或错误(在Coroutine内部)。
- 我们可以通过其事件循环直接注册所有回调(您可能会注意到,这些功能与JS中的
Promise
相似,但是虽然async在JS returnPromises
中的函数,而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,并执行由任务和其他未来对象注册的回调。
- 使用事件循环作为发动机,任务需要运行运行在单个Coroutine中包裹的异步作品的逻辑。任务寄回了带有事件循环的回调,以便这些回调触发coroutine直到完成。
- 多亏了类似于
yield
(最终与yield
相关的Coroutines结构),一系列异步执行可以作为单个工作单位组装,避免使用所谓的回调地狱。在Coroutine链的结束时,我们将遇到未来的对象,这些对象实际上给我们带来了预期的结果(或不幸的例外)。
请注意,我们没有关于 asyncio 库的讨论。我们没有提到诸如“ context”,“ schepend”或“信号”之类的概念,这些概念也是库的重要组成部分。我们之所以忽略他们的原因是,因为我们想知道与执行coroutine函数直接相关的对象在 asyncio 库中,以及它们如何一起工作,而不是研究一个的每个细节可能的工作流程。但是,我希望我也可以重新审视其余的概念,并在不久的将来充分描述整个图片。
结论
感谢您在这段漫长而漫长的旅程(双关语)上度过的时间。从深入研究生成器的CPYTHON源代码到探索Asyncio库,我们涵盖了相当数量的概念和实际代码。让我在这里简要列出它们:
- 发电机:迭代器,屈服,从简单的Coroutines
- coroutines:本地统治,等待
- asyncio :事件循环,任务,未来
配备了这些艰苦的知识,我们现在可以充满信心地使用async... await...
语法。我们涵盖的结构和概念不仅仅是Python。他们还将对其他编程语言和框架实现的异步功能提供深入的理解。