Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 3153 – 异步 IO 支持

作者:
Laurens Van Houtven <_ at lvh.cc>
状态:
已取代
类型:
标准跟踪
创建日期:
2011年5月29日
发布历史:

取代者:
3156

目录

摘要

本 PEP 描述了 Python 标准库的异步 IO 抽象。

目标是实现一个可以被许多不同的异步 IO 后端实现的抽象,并为库开发者提供一个目标,以便编写可以在这些不同后端之间移植的代码。

基本原理

目前想要在 Python 中编写异步代码的人有几种选择:

  • asyncoreasynchat
  • 某种定制的解决方案,最有可能基于 select 模块
  • 使用第三方库,例如 Twistedgevent

不幸的是,这些选择都有其缺点,本 PEP 试图解决这些问题。

尽管 asyncore 模块在 Python 标准库中存在已久,但由于其不灵活的 API,它存在根本性的缺陷,无法满足现代异步网络模块的期望。

此外,其方法过于简单,无法为开发者提供充分利用异步网络潜力所需的全部工具。

目前在生产环境中使用的最流行的解决方案是使用第三方库。这些库通常能提供令人满意的解决方案,但这些库之间缺乏兼容性,这往往导致代码库与所使用的库紧密耦合。

目前这种在不同异步 IO 库之间缺乏可移植性的情况导致了第三方库开发者大量的重复工作。一个足够强大的抽象可能意味着异步代码只需编写一次,即可在所有地方使用。

一个最终的附加目标是,标准库中对网络和线路协议的实现能够演变为真正的协议实现,而不是包罗万象的独立库,这些库会阻塞调用 recv()。这意味着它们可以轻松地在同步和异步代码中重用。

通信抽象

传输

传输(Transports)为从不同类型的连接读取字节和向不同类型的连接写入字节提供了统一的 API。本 PEP 中的传输始终是有序的、可靠的、双向的、面向流的、双端的连接。这可能是一个 TCP 套接字、一个 SSL 连接、一个管道(命名或非命名)、一个串口……它可能抽象 POSIX 平台上的文件描述符,或 Windows 上的 Handle,或特定平台适用的其他数据结构。它封装了使用该平台数据结构的所有具体实现细节,并为应用程序开发者提供了一个统一的接口。

传输与两件事通信:一方面是连接的另一端,另一方面是协议。它是特定底层传输机制和协议之间的桥梁。它的工作可以描述为允许协议只发送和接收字节,同时处理所有需要对这些字节进行的操作,以便最终通过线路发送。

传输的主要功能是向协议发送字节并从底层协议接收字节。写入传输使用 writewrite_sequence 方法完成。后一个方法是为了性能优化,允许软件利用某些传输机制中的特定功能。具体来说,这允许传输使用 writev 而不是 writesend,也称为散布/收集 IO。

传输可以暂停和恢复。这将导致它缓冲来自协议的数据,并停止将接收到的数据发送到协议。

传输也可以关闭、半关闭和中止。关闭的传输将完成将其队列中的所有数据写入底层机制,然后停止读写数据。中止传输会立即停止它,关闭连接而不发送任何仍处于队列中的数据。

进一步的写入操作将导致异常抛出。半关闭的传输不能再被写入,但仍可接收传入数据。

协议

协议可能对新用户来说更熟悉。该术语与您期望的协议一致:大多数人首先想到的协议,如 HTTP、IRC、SMTP……都是可以通过协议实现的示例。

协议最简短有用的定义是传输和应用程序其余逻辑之间的(通常是双向的)桥梁。协议将从传输接收字节,并将该信息翻译成某种行为,通常会触发对象上的一些方法调用。类似地,应用程序逻辑调用协议上的一些方法,协议将这些方法翻译成字节并与传输进行通信。

最简单的协议之一是基于行的协议,其中数据由 \r\n 分隔。协议将从传输接收字节并缓冲它们,直到至少有一个完整的行。一旦完成,它会将该行传递给某个对象。理想情况下,这可以通过可调用对象或协议组合的完全独立的对象来实现,但也可以通过子类化来实现(Twisted 的 LineReceiver 就是这种情况)。对于另一个方向,协议可以有一个 write_line 方法,该方法添加所需的 \r\n 并将新的字节缓冲区传递给传输。

本 PEP 建议了一个名为 ChunkProtocol 的通用 LineReceiver,其中“块”是流中的一个消息,由指定的分隔符分隔。实例接受一个分隔符和一个可调用对象,该可调用对象将在收到数据块时被调用(与 Twisted 的子类化行为相反)。ChunkProtocol 还有一个 write_chunk 方法,类似于上面描述的 write_line 方法。

为什么需要分离协议和传输?

协议和传输之间的这种分离经常让初次接触它的人感到困惑。事实上,标准库本身在很多情况下并没有做出这种区分,尤其是在它向用户提供的 API 中。

尽管如此,这是一个非常有用的区别。在最坏的情况下,它通过清晰的关注点分离简化了实现。然而,它通常服务于一个更有用的目的,即能够在不同的传输之间重用协议。

考虑一个简单的 RPC 协议。相同的字节可以通过许多不同的传输传输,例如管道或套接字。为了达到这个目的,我们将协议与传输分开。协议只读取和写入字节,并且并不真正关心最终传输这些字节的机制。

这还允许协议轻松地堆叠或嵌套,从而实现更多的代码重用。一个常见的例子是 JSON-RPC:根据规范,它可以通过套接字和 HTTP [1] 进行通信。实际上,它往往主要封装在 HTTP 中。协议-传输抽象允许我们构建一个协议和传输的堆栈,使您能够像使用传输一样使用 HTTP。对于 JSON-RPC,这可能得到一个类似以下的堆栈:

  1. TCP 套接字传输
  2. HTTP 协议
  3. 基于 HTTP 的传输
  4. JSON-RPC 协议
  5. 应用程序代码

流量控制

消费者

消费者消费生产者产生的数据。它们与生产者一起,使得流量控制成为可能。

消费者主要在流量控制中扮演被动角色。每当生产者有可用数据时,它们就会被调用。然后,它们处理这些数据,并通常将控制权交还给生产者。

消费者通常实现某种形式的缓冲区。它们通过告知生产者这些缓冲区的当前状态来使流量控制成为可能。消费者可以指示生产者完全停止生产,暂时停止生产,或者在之前被告知暂停的情况下恢复生产。

生产者使用 register 方法向消费者注册。

生产者

消费者消耗字节,而生产者则产生字节。

生产者仿照 Twisted 中找到的 IPushProducer 接口进行建模。尽管也有 IPullProducer,但总体而言它远不那么有趣,因此可能超出本 PEP 的范围。

尽管可以告诉生产者完全停止生产,但它们最有用的两个方法是 pauseresume。这些通常由消费者调用,以指示它是否准备好处理(“消耗”)更多数据。消费者和生产者合作,使流量控制成为可能。

除了 Twisted 的 IPushProducer 接口之外,生产者还有一个 half_register 方法,当消费者尝试注册该生产者时,该方法会与消费者一起被调用。在大多数情况下,这只是设置 self.consumer = consumer 的情况,但某些生产者在注册消费者时可能需要更复杂的先决条件或行为。最终用户不应直接调用此方法。

考虑过的 API 替代方案

生成器作为生产者

有人建议使用生成器来实现生产者。然而,这似乎存在一些问题。

首先,存在一个概念性问题。生成器在某种意义上是“被动的”。它需要通过方法调用来告诉它采取行动。生产者是“主动的”:它发起这些方法调用。真正的生产者与它们的消费者之间存在对称关系。在生成器变成生产者的例子中,只有消费者拥有引用,而生产者对消费者的存在一无所知。

这个问题也转化为一些技术问题。在成功调用其消费者的 write 方法后,(推送)生产者可以再次采取行动。在生成器的情况下,需要通过迭代协议(一个可能无限期阻塞的过程)来请求下一个对象,或者通过向其抛出某种信号异常来告知它。

这种信号设置可能提供一个技术上可行的解决方案,但仍然不令人满意。一方面,这会在消费者中引入不必要的复杂性,因为它不仅需要了解如何接收和处理数据,还需要了解如何请求新数据以及如何处理没有可用新数据的情况。

后一种边缘情况尤其成问题。需要处理它,因为不允许整个操作阻塞。但是,生成器在迭代时不能在不终止的情况下抛出异常,从而丢失生成器的状态。结果,需要使用哨兵值来指示可用数据的缺失,而不是使用异常机制。

最后但并非最不重要的是,没有人实际编写工作代码来演示如何使用它们。

参考资料


来源:https://github.com/python/peps/blob/main/peps/pep-3153.rst

最后修改:2025-02-01 08:59:27 GMT