asyncio 实际上是如何工作的?

2024-03-31

这个问题是由我的另一个问题引发的:如何在cdef中等待? https://stackoverflow.com/questions/48989065/how-to-await-in-cdef

网络上有大量关于asyncio,但它们都非常肤浅。我找不到任何有关如何操作的信息asyncio实际实现了什么,以及什么使 I/O 异步。我试图阅读源代码,但它是数千行不是最高等级的 C 代码,其中很多涉及辅助对象,但最关键的是,很难将 Python 语法和它要翻译的 C 代码联系起来进入。

Asycnio 自己的文档就更没有帮助了。那里没有关于它如何工作的信息,只有一些关于如何使用它的指南,这些指南有时也具有误导性/写得很糟糕。

我熟悉 Go 的协程实现,并且希望 Python 也能做同样的事情。如果是这样的话,我在上面链接的帖子中提出的代码就会起作用。既然没有,我现在正在尝试找出原因。目前我的猜测如下,有错误的地方请指正:

  1. 表单的过程定义async def foo(): ...实际上被解释为继承的类的方法coroutine.
  2. 也许,async def实际上分为多种方法await语句,其中调用这些方法的对象能够跟踪迄今为止的执行进度。
  3. 如果上述情况成立,那么本质上,协程的执行可以归结为由某个全局管理器(循环?)调用协程对象的方法。
  4. 全局管理器以某种方式(如何?)知道 I/O 操作何时由 Python(仅?)代码执行,并且能够在当前执行方法放弃控制后选择待执行的协程方法之一来执行(点击await陈述)。

换句话说,这是我对一些“脱糖”的尝试asyncio语法转换为更容易理解的内容:

async def coro(name):
    print('before', name)
    await asyncio.sleep()
    print('after', name)

asyncio.gather(coro('first'), coro('second'))

# translated from async def coro(name)
class Coro(coroutine):
    def before(self, name):
        print('before', name)

    def after(self, name):
        print('after', name)

    def __init__(self, name):
        self.name = name
        self.parts = self.before, self.after
        self.pos = 0

    def __call__():
        self.parts[self.pos](self.name)
        self.pos += 1

    def done(self):
        return self.pos == len(self.parts)


# translated from asyncio.gather()
class AsyncIOManager:

    def gather(*coros):
        while not every(c.done() for c in coros):
            coro = random.choice(coros)
            coro()

如果我的猜测被证明是正确的:那么我就有问题了。在这种情况下 I/O 实际上是如何发生的?在单独的线程中?整个解释器是否暂停并且 I/O 发生在解释器外部? I/O 到底是什么意思?如果我的 python 程序名为 Copen()过程,然后它向内核发送中断,放弃对它的控制,Python解释器如何知道这一点并能够继续运行其他代码,而内核代码执行实际的I/O,直到它唤醒Python过程最初是哪个发送中断的?原则上Python解释器如何知道这种情况的发生?


异步如何工作?

在回答这个问题之前,我们需要了解一些基本术语,如果您已经知道其中任何一个,请跳过这些术语。

发电机 https://wiki.python.org/moin/Generators

生成器是允许我们暂停 python 函数执行的对象。用户策划的生成器是使用关键字实现的yield https://docs.python.org/3/reference/expressions.html#yield-expressions。通过创建一个包含以下内容的普通函数yield关键字,我们将该函数转换为生成器:

>>> def test():
...     yield 1
...     yield 2
...
>>> gen = test()
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

如您所见,调用next() https://docs.python.org/3/library/functions.html#next生成器上的 导致解释器加载测试的框架,并返回yield编辑值。呼唤next()再次,导致帧再次加载到解释器堆栈中,并继续yielding另一个值。

到了第三次next()被调用,我们的生成器完成了,并且StopIteration https://docs.python.org/3/library/exceptions.html#StopIteration被扔了。

与发电机通信

生成器的一个鲜为人知的功能是您可以使用两种方法与它们进行通信:send() https://docs.python.org/3/reference/expressions.html#generator.send and throw() https://docs.python.org/3/reference/expressions.html#generator.throw.

>>> def test():
...     val = yield 1
...     print(val)
...     yield 2
...     yield 3
...
>>> gen = test()
>>> next(gen)
1
>>> gen.send("abc")
abc
2
>>> gen.throw(Exception())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in test
Exception

致电后gen.send(),该值作为返回值传递yield关键词。

gen.throw()另一方面,允许在生成器内部抛出异常,并在同一位置引发异常yield被称为。

从生成器返回值

从生成器返回一个值,结果将该值放入StopIteration例外。我们稍后可以从异常中恢复该值并将其用于我们的需要。

>>> def test():
...     yield 1
...     return "abc"
...
>>> gen = test()
>>> next(gen)
1
>>> try:
...     next(gen)
... except StopIteration as exc:
...     print(exc.value)
...
abc

看哪,一个新关键字:yield from

Python 3.4 添加了一个新关键字:yield from https://docs.python.org/3/reference/expressions.html#yield-expressions。该关键字允许我们做的就是传递任何next(), send() and throw()进入最内层的嵌套生成器。如果内部生成器返回一个值,它也是yield from:

>>> def inner():
...     inner_result = yield 2
...     print('inner', inner_result)
...     return 3
...
>>> def outer():
...     yield 1
...     val = yield from inner()
...     print('outer', val)
...     yield 4
...
>>> gen = outer()
>>> next(gen)
1
>>> next(gen) # Goes inside inner() automatically
2
>>> gen.send("abc")
inner abc
outer 3
4

我写过一篇文章 https://towardsdatascience.com/cpython-internals-how-do-generators-work-ba1c4405b4bc进一步阐述这个话题。

把它们放在一起

引入新关键字后yield from在Python 3.4中,我们现在能够在生成器内部创建生成器,就像隧道一样,将数据从最里面的生成器来回传递到最外面的生成器。这为发电机产生了新的含义——协程.

协程是可以在运行时停止和恢复的函数。在 Python 中,它们是使用以下方式定义的async def https://docs.python.org/3/reference/compound_stmts.html#coroutine-function-definition关键词。就像发电机一样,它们也使用自己的形式yield from这是await https://docs.python.org/3/reference/expressions.html#await。前async and await在 Python 3.5 中引入,我们以与创建生成器完全相同的方式创建协程(使用yield from代替await).

async def inner():
    return 1

async def outer():
    await inner()

就像所有迭代器和生成器都实现__iter__()方法,所有协程都实现__await__()这让他们每次都能继续await coro叫做。

有一个不错的顺序图 https://docs.python.org/3.5/_images/tulip_coro.png在 - 的里面Python 文档 https://docs.python.org/3.5/library/asyncio-task.html#example-chain-coroutines你应该检查一下。

在 asyncio 中,除了协程函数之外,我们还有 2 个重要的对象:tasks and futures.

Futures https://docs.python.org/3/library/asyncio-task.html#asyncio.Future

期货是具有__await__()方法实施,他们的工作是保持一定的状态和结果。状态可以是以下之一:

  1. PENDING - future 没有任何结果或异常集。
  2. 取消 - 未来被取消使用fut.cancel()
  3. FINISHED - 未来已经完成,或者通过使用结果集fut.set_result() https://docs.python.org/3/library/asyncio-task.html#asyncio.Future.set_result或通过使用异常设置fut.set_exception() https://docs.python.org/3/library/asyncio-task.html#asyncio.Future.set_exception

正如您所猜测的,结果可以是返回的 Python 对象,也可以是可能引发的异常。

Another 重要的的特征future对象,是它们包含一个名为的方法add_done_callback() https://docs.python.org/3/library/asyncio-task.html#asyncio.Future.add_done_callback。此方法允许任务完成后立即调用函数 - 无论是引发异常还是完成。

Tasks https://docs.python.org/3/library/asyncio-task.html#task

任务对象是特殊的 future,它包裹着协程,并与最里面和最外面的协程进行通信。每次协程await是一个未来,未来会一路回到任务(就像yield from),任务收到它。

接下来,任务将自身与未来联系起来。它通过调用来做到这一点add_done_callback()关于未来。从现在开始,如果未来将要完成,通过取消、传递异常或传递 Python 对象作为结果,任务的回调将被调用,并且它将恢复存在。

Asyncio

我们必须回答的最后一个紧迫问题是——IO 是如何实现的?

在 asyncio 的深处,我们有一个事件循环。任务的事件循环。事件循环的工作是在每次任务准备好时调用它们,并将所有工作协调到一台工作机器中。

事件循环的 IO 部分构建在一个名为的关键函数之上select https://docs.python.org/3/library/select.html#module-select。 Select 是一个阻塞函数,由底层操作系统实现,允许在套接字上等待传入或传出数据。接收到数据后,它会唤醒,并返回已接收数据的套接字,或准备写入的套接字。

当您尝试通过 asyncio 通过套接字接收或发送数据时,下面实际发生的情况是首先检查套接字是否有任何可以立即读取或发送的数据。如果它是.send()缓冲区已满,或者.recv()缓冲区为空,套接字已注册到select函数(只需将其添加到列表之一,rlist for recv and wlist for send)和适当的函数await是一个新创建的future对象,绑定到该套接字。

当所有可用任务都在等待 future 时,事件循环调用select并等待。当其中一个套接字有传入数据时,或其send缓冲区耗尽后,asyncio 检查绑定到该套接字的未来对象,并将其设置为完成。

现在所有的魔法都发生了。未来已经完成,之前添加的任务add_done_callback()复活并呼唤.send()在恢复最里面的协程的协程上(因为await链),并且您从附近的缓冲区读取新接收到的数据。

再次方法链,以防出现recv():

  1. select.select waits.
  2. 一个准备好的套接字,并返回数据。
  3. 来自套接字的数据被移动到缓冲区中。
  4. future.set_result()叫做。
  5. 添加自身的任务add_done_callback()现在已经醒了。
  6. 任务调用.send()在协程上,它一直进入最里面的协程并唤醒它。
  7. 数据正在从缓冲区中读取并返回给我们谦虚的用户。

总之,asyncio 使用生成器功能,允许暂停和恢复功能。它用yield from允许从最里面的生成器到最外面的生成器来回传递数据的功能。它使用所有这些来在等待 IO 完成时停止函数执行(通过使用操作系统select功能)。

最重要的是?当一个功能暂停时,另一个功能可能会运行并与精致的结构交错,这是异步的。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

asyncio 实际上是如何工作的? 的相关文章

  • cv2.drawContours() - 取消填充字符内的圆圈(Python,OpenCV)

    根据 Silencer的建议 我使用了他发布的代码here https stackoverflow com questions 48244328 copy shape to blank canvas opencv python 482465
  • PyTorch 给出 cuda 运行时错误

    我对我的代码做了一些小小的修改 以便它不使用 DataParallel and DistributedDataParallel 代码如下 import argparse import os import shutil import time
  • ImportError:运行 jupyter Notebook 时没有名为 IPython.paths 的模块?

    我通过以下方式安装了 jupyter usr local opt python bin python2 7 m pip install jupyter 这将安装 ipython 版本 4 1 2 但是 当我运行 jupyter Notebo
  • 更改 x 轴比例

    我使用 Matlab 创建了这个图 使用 matplotlib x 轴绘制大数字 例如 100000 200000 300000 我想要 1 2 3 和 10 5 之类的值来指示它实际上是 100000 200000 300000 有没有一
  • ValueError:不支持连续[重复]

    这个问题在这里已经有答案了 我正在使用 GridSearchCV 进行线性回归的交叉验证 不是分类器也不是逻辑回归 我还使用 StandardScaler 对 X 进行标准化 我的数据框有 17 个特征 X 和 5 个目标 y 观察 约11
  • 如何从下面的html中提取数据?

    我想要从中提取数据的 Html 是 div class infoMessageInner p span class ng binding Fiber r best lld till adressen Tj nsterna kan du be
  • 如何使用 javascript/jquery/AJAX 调用 Django REST API?

    我想使用 Javascript jQuery AJAX 在前端调用 Django Rest API 请求方法是 POST 但当我看到 API 调用它的调用 OPTIONS 方法时 所以 我开始了解access control allow o
  • CNTK 抱怨 LSTM 中的动态轴

    我正在尝试在 CNTK 中实现 LSTM 使用 Python 来对序列进行分类 Input 特征是固定长度的数字序列 时间序列 标签是 one hot 值的向量 Network input input variable input dim
  • 在相同任务上,Keras 比 TensorFlow 慢

    我正在使用 Python 运行斩首 DCNN 本例中为 Inception V3 来获取图像特征 我使用的是 Anaconda Py3 6 和 Windows7 使用 TensorFlow 时 我将会话保存在变量中 感谢 jdehesa 并
  • Alembic:如何迁移模型中的自定义类型?

    My User模型是 class User UserMixin db Model tablename users noinspection PyShadowingBuiltins uuid Column uuid GUID default
  • Python、subprocess、call()、check_call 和 returncode 来查找命令是否存在

    我已经弄清楚如何使用 call 让我的 python 脚本运行命令 import subprocess mycommandline lumberjack sleep all night work all day subprocess cal
  • 使用 Conda 更新特定模块会删除大量软件包

    我最近开始使用 Anaconda Python 发行版 因为它提供了许多开箱即用的数据分析库 使用 conda 创建环境和安装软件包也轻而易举 但是当我想更新 Python 本身或任何其他模块时 我遇到了一些严重的问题 我事先被告知我的很多
  • Werkzeug 中的线程和本地代理。用法

    首先 我想确保我正确理解了功能的分配 分配本地代理功能以通过线程内的模块 包 共享变量 对象 我对吗 其次 用法对我来说仍然不清楚 也许是因为我误解了作业 我用烧瓶 如果我有两个 或更多 模块 A B 我想将对象C从模块A导入到模块B 但我
  • 为什么我应该使用 WSGI?

    使用 mod python 一段时间了 我读了越来越多关于 WSGI 有多好的文章 但没有真正理解为什么 那么我为什么要切换到它呢 有什么好处 这很难吗 学习曲线值得吗 为了用 Python 开发复杂的 Web 应用程序 您可能会使用更全面
  • `pyqt5'错误`元数据生成失败`

    我正在尝试安装pyqt5使用带有 M1 芯片和 Python 3 9 12 的 mac 操作系统 我怀疑M1芯片可能是原因 我收到一个错误metadata generation failed 最小工作示例 directly in the t
  • Flask 应用程序的测试覆盖率不起作用

    您好 想在终端的 Flask 应用程序中测试 删除路由 我可以看到测试已经过去 它说 test user delete test app LayoutTestCase ok 但是当我打开封面时 它仍然是红色的 这意味着没有覆盖它 请有人向我
  • sqlite3从打印数据中删除括号

    我创建了一个脚本 用于查找数据库第一行中的最后一个值 import sqlite3 global SerialNum conn sqlite3 connect MyFirstDB db conn text factory str c con
  • 附加两个具有相同列、不同顺序的数据框

    我有两个熊猫数据框 noclickDF DataFrame 0 123 321 0 1543 432 columns click id location clickDF DataFrame 1 123 421 1 1543 436 colu
  • bs4 `next_sibling` VS `find_next_sibling`

    我在使用时遇到困难next sibling 并且类似地与next element 如果用作属性 我不会得到任何返回 但如果用作find next sibling or find next 然后就可以了 来自doc https www cru
  • 使用 Python 将对象列表转为 JSON

    我在转换时遇到问题Object实例到 JSON ob Object list name scaping myObj base url u number page for ob in list name json string json du

随机推荐