PEP 3153 – 异步IO支持
- 作者:
- Laurens Van Houtven <_ at lvh.cc>
- 状态:
- 已取代
- 类型:
- 标准跟踪
- 创建:
- 2011年5月29日
- 发布历史:
- 取代版本:
- 3156
摘要
本PEP描述了Python标准库中异步IO的抽象。
目标是实现一个可以由许多不同的异步IO后端实现的抽象,并为库开发者提供一个目标,以便编写可在这些不同后端之间移植的代码。
基本原理
目前想要在Python中编写异步代码的人有一些选择
不幸的是,这些选项都有其缺点,本PEP试图解决这些问题。
尽管asyncore模块已成为Python标准库的一部分很长时间,但它存在一些根本缺陷,这些缺陷源于一个不灵活的API,该API无法满足现代异步网络模块的期望。
此外,其方法过于简单,无法为开发人员提供充分利用异步网络潜力的所有工具。
目前在生产中使用最广泛的解决方案涉及使用第三方库。这些库通常提供令人满意的解决方案,但这些库之间缺乏兼容性,这往往使代码库与它们使用的库紧密耦合。
目前不同异步IO库之间缺乏可移植性导致第三方库开发者重复工作很多。一个足够强大的抽象意味着异步代码可以编写一次,但可以在任何地方使用。
最终的目标是,标准库中线路和网络协议的实现将演变为真正的协议实现,而不是包含所有内容(包括阻塞式调用 recv()
)的独立库。这意味着它们可以轻松地重用于同步和异步代码。
通信抽象
传输
传输为从不同类型的连接读取字节和写入字节提供统一的API。本PEP中的传输始终是有序的、可靠的、双向的、面向流的两端点连接。这可能是TCP套接字、SSL连接、管道(命名或其他)、串行端口……它可能抽象化POSIX平台上的文件描述符或Windows上的句柄或其他适合特定平台的数据结构。它封装了使用该平台数据结构的所有特定实现细节,并为应用程序开发人员提供统一的接口。
传输与两件事通信:一方面是连接的另一端,另一方面是协议。它是特定底层传输机制和协议之间的桥梁。其工作可以描述为允许协议仅发送和接收字节,并处理那些字节最终通过线路发送所需的所有操作。
传输的主要功能是将字节发送到协议并从底层协议接收字节。写入传输是使用 write
和 write_sequence
方法完成的。后者方法是一种性能优化,允许软件利用某些传输机制中的特定功能。具体来说,这允许传输使用 writev 而不是 write 或 send,也称为分散/收集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,这可能会为您提供一个类似这样的堆栈
- TCP套接字传输
- HTTP协议
- 基于HTTP的传输
- JSON-RPC协议
- 应用程序代码
流量控制
消费者
消费者消费由生产者产生的字节。它们与生产者一起使流量控制成为可能。
消费者在流量控制中主要扮演被动角色。每当生产者有数据可用时,它们就会被调用。然后它们处理这些数据,通常会将控制权交还给生产者。
消费者通常实现某种缓冲区。它们通过告诉生产者这些缓冲区的当前状态来实现流量控制。消费者可以指示生产者完全停止生产、暂时停止生产,或者如果之前被告知暂停,则恢复生产。
使用 register
方法将生产者注册到消费者。
生产者
在消费者消费字节的地方,生产者产生字节。
生产者是根据Twisted中找到的 IPushProducer 接口建模的。尽管也有 IPullProducer,但总体而言,它远没有那么有趣,因此可能超出了本PEP的范围。
尽管可以告诉生产者完全停止生产,但它们拥有的两个最有趣的方法是 pause
和 resume
。这些通常由消费者调用,以表示它是否准备好处理(“消费”)更多数据。消费者和生产者协作以实现流量控制。
除了Twisted IPushProducer 接口之外,生产者还有一个 half_register
方法,该方法在消费者尝试注册该生产者时会与消费者一起调用。在大多数情况下,这仅仅是设置 self.consumer = consumer
,但某些生产者在注册消费者时可能需要更复杂的先决条件或行为。最终用户不应该直接调用此方法。
考虑的API替代方案
生成器作为生产者
有人建议使用生成器来实现生产者。但是,这似乎存在一些问题。
首先,存在一个概念上的问题。从某种意义上说,生成器是“被动的”。它需要通过方法调用来被告知采取行动。生产者是“主动的”:它发起这些方法调用。真正的生产者与其消费者之间存在对称关系。在生成器变成生产者的情况下,只有消费者会持有引用,而生产者对此毫不知情。
这个概念问题也转化为了一些技术问题。在消费者上成功调用 write
方法后,(推)生产者可以再次自由地采取行动。在生成器的情况下,它需要被告知,要么通过迭代协议请求下一个对象(这个过程可能会无限期阻塞),要么可能通过向其抛出某种信号异常。
这种信号设置可能提供了一个技术上可行的解决方案,但它仍然不令人满意。一方面,这增加了消费者的不必要复杂性,消费者现在不仅需要理解如何接收和处理数据,还需要理解如何请求新数据以及处理没有新数据可用情况。
后一种边缘情况尤其成问题。它需要得到解决,因为整个操作不允许阻塞。然而,生成器在迭代时不能引发异常而不会终止,从而丢失生成器的状态。因此,信号缺少可用数据必须使用哨兵值来完成,而不是使用异常机制。
最后但并非最不重要的一点是,没有人真正编写出演示如何使用它们的代码。
参考文献
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-3153.rst