asyncio:为什么默认情况下它不是非阻塞的

asyncio: why isn't it non-blocking by default

默认情况下,asyncio 同步运行协程。如果它们包含阻塞 IO 代码,它们仍然会等待 return。解决此问题的方法是 loop.run_in_executor(),它将代码转换为线程。如果一个线程阻塞在 IO 上,另一个线程可以开始执行。这样您就不会浪费时间等待 IO 调用。

如果您在没有执行程序的情况下使用 asyncio,则会失去这些加速。所以我想知道,为什么你必须明确地使用执行者。为什么不默认启用它们? (下面我将重点介绍http请求,但它们实际上只是一个例子。我对一般原则感兴趣。)

经过一番搜索,我找到了 aiohttp。它是一个本质上提供 asynciorequests 组合的库:非阻塞 HTTP 调用。对于执行器,asynciorequests 的行为与 aiohttp 非常相似。是否有实施新库的理由,您是否因使用执行程序而付出性能损失?

这个问题得到了回答: Mikhail Gerasimov 向我解释说,执行器会启动 OS-threads,它们可能会变得昂贵。因此,不将它们作为默认行为是有道理的。 aiohttp 比在执行器中使用 requests 模块更好,因为它提供了只有协程的非阻塞代码。

这让我想到了这个问题。 aiohttp 将自己宣传为:

Asynchronous HTTP Client/Server for asyncio and Python.

所以aiohttp是基于asyncio?为什么 asyncio 不提供只有协程的非阻塞代码呢?这将是理想的默认设置。

或者 aiohttp 自己实现了这个新的事件循环(没有 OS-threads)? 在那种情况下,我不明白为什么他们会根据 asyncio 来宣传自己。 Async/await 是一种语言功能。 Asyncio 是一个事件循环。如果 aiohttp 有自己的事件循环,那么与 asyncio 应该没有什么交集。实际上,我认为这样的事件循环将是一个比 http 请求更大的功能。

asyncio 是异步的,因为协程 自愿合作 所有 asyncio 编写代码必须考虑到合作,这就是重点。否则你还不如专门使用线程来实现并发。

你不能在执行器中 运行 'blocking' 函数(非协程函数或不合作的方法)因为你不能只是 assume 该代码 可以 在单独的执行程序线程中 运行。或者即使它需要成为执行者中的运行。

Python 标准库充满了非常有用的代码,asyncio 项目将要使用这些代码。标准库的大部分由常规的 'blocking' 函数和 class 定义组成。他们工作很快,所以即使他们 'block',他们也会在合理的时间内 return。

但是大部分代码也不是线程安全的,通常不需要。但是一旦 asyncio 会 运行 执行器中的所有此类代码 自动 ,那么您就不能再使用非线程安全函数了。此外,为 运行 中的同步代码创建一个线程不是免费的,创建线程对象需要时间,而且你的 OS 也不会让你 运行 无限数量的线程。标准库函数和方法的负载是 fast,为什么要在单独的线程中 运行 str.splitlines()urllib.parse.quote() 呢?更快地执行代码并完成它?

你可能会说那些函数没有按照你的标准阻塞。这里你没有定义'blocking',但是'blocking'只是表示:不会主动屈服。。如果我们将其缩小到当它必须等待某事并且计算机可能正在做其他事情时不会自愿屈服,那么下一个问题将是如何你会检测到它应该已经产生吗?

答案是 你不能。 time.sleep() 是一个阻塞函数,你想屈服于循环 for,但这是C函数调用。 Python 无法 知道 time.sleep() 将阻塞更长时间,因为调用 time.sleep() 的函数将查找名称 time 在全局命名空间中,然后是名称查找结果上的属性 sleep,仅当实际执行 time.sleep() 表达式时。因为 Python 的命名空间可以在执行期间的任何时候更改 ,所以在您实际执行该函数之前,您无法知道 time.sleep() 会做什么。

你可以说 time.sleep() 实现应该在调用时自动让步,但你必须开始识别所有这些函数。而且你必须修补的地方数量没有限制,你永远不可能知道所有的地方。当然不适用于第三方库。例如,python-adb project 使用 libusb1 库为您提供与 Android 设备的同步 USB 连接。这不是标准的 I/O 代码路径,所以 Python 怎么知道创建和使用这些连接是产生收益的好地方?

所以你不能假设代码需要在一个执行器中是运行,并不是所有的代码都可以在一个执行器中是运行因为它不是线程安全的,并且 Python 无法检测到代码何时阻塞并且应该真正让步。

那么asyncio下的协程是如何协作的呢?通过使用 task objects per logical piece of code that needs to run concurrently with other tasks, and by using future objects 向任务发出信号,表明当前逻辑代码段想要将控制权让给其他任务。这就是异步 asyncio 代码异步、自愿放弃控制的原因。当循环将控制权交给多个任务中的一个任务时,该任务将执行协程调用链中的单个 'step',直到该调用链产生一个未来对象,此时任务会添加一个 唤醒 对未来对象的回调 'done' 回调列表和 return 对循环的控制。稍后,当未来被标记为完成时,唤醒回调是 运行 并且任务将执行另一个协程调用链步骤。

Something else 负责将未来的对象标记为已完成。当您使用 asyncio.sleep() 时,将在特定时间为 运行 的回调提供给循环,该回调会将 asyncio.sleep() 未来标记为已完成。当您使用 stream object to perform I/O, then (on UNIX), the loop uses select calls to detect when it is time to wake up a future object when the I/O operation is done. And when you use a lock or other synchronisation primitive 时,同步原语将维护一堆未来以在适当的时候标记为 'done' (等待锁?向堆中添加未来。释放持有的锁?选择堆中的下一个未来并将其标记为已完成,因此等待锁的下一个任务可以唤醒并获取锁等)。

将阻塞的同步代码放入执行程序只是这里合作的另一种形式。在项目中使用 asyncio 时,由 开发人员 确保您使用提供给您的工具来确保您的协程协作。您可以自由地对文件使用阻塞 open() 调用而不是使用流,并且当您知道代码需要在单独的线程中 运行 以避免阻塞太久时,您可以自由地使用执行器。

最后但同样重要的是,使用 asyncio 的全部意义在于 避免 尽可能多地使用线程。使用线程有缺点;代码需要线程安全(控制可以在线程之间切换任何地方,所以访问共享数据的两个线程应该小心, 'taking care' 可能意味着代码 变慢了 )。线程不管有没有事都会执行;在 all 等待 I/O 发生的固定数量的线程之间切换控制是浪费 CPU 时间,其中 asyncio 循环是空闲的找到一个没有等待的任务。

So aiohttp is based on asyncio?

是的,它建立在 asyncio 的抽象之上,例如 futures, transports and protocols, synchronization primitives,等等。

Why doesn't asyncio offer non-blocking code with only coroutines then?

如果您使用 asyncio API,那正是它的作用。它在单独的线程池中为 connect to a server, resolve a host name, create a server, and even run blocking code 提供非阻塞代码,而不会阻塞事件循环。

aiohttp 使用所有这些功能在 asyncio 之上实现功能强大的 HTTP 客户端和服务器。

Or did aiohttp implement this new event-loop (without OS-threads) itself ?

不,aiohttp 挂钩到 asyncio 的事件循环中。更准确地说,使用 aiohttp 的 应用程序 启动 asyncio 事件循环并将 aiohttp(和其他基于 asyncio 的库)挂钩到其中。

Async/await are a language feature. Asyncio is an event-loop.

Async/await 是一种语言特性,就像生成器一样。 Asyncio 是一个使用它们的库,例如 itertools。还有其他使用协程的库,例如curio and trio.