在上一节中,我们介绍了多线程作为实现 I/O 操作并发性的一种机制。但是,我们不需要多个线程来实现这种并发性。我们可以在一个进程和一个线程的范围内完成所有这一切。我们利用这样一个事实:在系统层面,I/O 操作可以并发完成。为了更好地理解这一点,我们需要深入了解套接字的工作原理,特别是非阻塞套接字的工作原理。
套接字是用于通过网络发送和接收数据的低级抽象。它是数据在服务器之间传输的基础。套接字支持两个主要操作:发送字节和接收字节。我们将字节写入套接字,然后这些字节将被发送到远程地址,通常是某种类型的服务器。一旦我们发送了这些字节,我们就等待服务器将其响应写回我们的套接字。一旦这些字节被发送回我们的套接字,我们就可以读取结果。
套接字是一个低级概念,如果你把它们想象成邮箱,就很容易理解。你可以把一封信放进你的邮箱,然后邮递员拿起并递送到收件人的邮箱。收件人打开他们的邮箱和你的信。根据内容,收件人可能会回信给你。在这个类比中,你可以把信看作是我们想要发送的数据或字节。把信放入邮箱的行为可以看作是将字节写入套接字,而打开邮箱读信可以看作是从套接字读取字节。邮递员可以看作是互联网上的传输机制,将数据路由到正确的地址。
在我们之前看到的获取 example.com 内容的情况下,我们打开一个连接到 example.com 服务器的套接字。然后我们将获取内容的请求写入该套接字,并等待服务器回复结果:在这种情况下,网页的 HTML。我们可以在图 1.7 中可视化字节流向和流出服务器的流程。
套接字默认是阻塞的。简单地说,这意味着当我们等待服务器回复数据时,我们会停止或阻塞我们的应用程序,直到我们获得数据读取。因此,在我们的应用程序收到服务器数据、发生错误或超时之前,我们的应用程序停止运行任何其他任务。
在操作系统层面,我们不需要这样做。套接字可以在非阻塞模式下运行。在非阻塞模式下,当我们将字节写入套接字时,我们可以只是发送后忘记写入或读取,我们的应用程序可以继续执行其他任务。稍后,我们可以让操作系统告诉我们收到了字节,并在那时处理它。这让应用程序在我们等待字节返回时可以做任何数量的事情。我们变得更加反应式,让操作系统通知我们有数据需要处理,而不是阻塞并等待数据到来。
在后台,这是由几种不同的事件通知系统执行的,具体取决于我们运行的操作系统。asyncio 足够抽象,可以在不同的通知系统之间切换,取决于我们的操作系统支持哪个。以下是特定操作系统使用的事件通知系统:
- IOCP (I/O 完成端口) — Windows
这些系统跟踪我们的非阻塞套接字,并在它们准备好供我们操作时通知我们。这个通知系统是 asyncio 实现并发的基础。在 asyncio 的并发模型中,我们在任何给定时间只有一个线程在执行 Python。当我们遇到一个 I/O 操作时,我们将其移交给操作系统的事件通知系统来为我们跟踪。一旦我们完成了这个交接,我们的 Python 线程就可以自由运行其他 Python 代码,或者添加更多非阻塞套接字供操作系统为我们跟踪。当我们的 I/O 操作完成时,我们“唤醒”正在等待该结果的任务,然后继续运行该 I/O 操作之后的任何其他 Python 代码。我们可以在图 1.8 中用几个各自依赖套接字的独立操作来可视化这个流程。
图 1.8 发出非阻塞 I/O 请求会立即返回并告诉操作系统监视套接字获取数据。这使得 execute_other_code() 可以立即运行,而不是等待 I/O 请求完成。稍后,当 I/O 完成时,我们可以得到警报并处理响应。
但是我们如何跟踪哪些任务在等待 I/O 与哪些任务只是运行常规 Python 代码呢?答案在于一个称为事件循环的构造。