我们什么时候应该在 Python 中使用 asyncio?
1. 在 Python 中使用 Asyncio 的原因
在 Python 项目中使用 asyncio 可能有 3 个原因:
- 使用 asyncio 以便在您的程序中采用协程。
- 使用 asyncio 以使用异步编程范例。
- 使用 asyncio 以使用非阻塞 I/O。
1.1. 使用协程
我们可能会选择使用 asyncio,因为我们要使用协程。我们可能想要使用协程,因为我们的程序中可以有比并发线程更多的并发协程。协程是另一个并发单元,就像线程和进程一样。
基于线程的并发由线程模块提供,并由底层操作系统支持。它适用于阻塞 I/O 任务,例如从文件、套接字和设备读取和写入。
基于进程的并发由 multiprocessing 模块提供,也由底层操作系统支持,如线程。它适用于不需要太多进程间通信的 CPU 绑定任务,例如计算任务。
协程是 Python 语言和运行时(标准解释器)提供的替代方案,并由 asyncio 模块进一步支持。它们适用于具有子进程和套接字的非阻塞 I/O,但是,阻塞 I/O 和 CPU 绑定任务可以在幕后使用线程和进程以模拟非阻塞方式使用。
最后一点是微妙而关键的。虽然我们可以选择使用协同程序来实现它们引入 Python 的非阻塞功能,但实际上我们可以将它们用于任何任务。如果我们愿意,任何使用线程或进程编写的程序都可以重写或使用协程编写。
线程和进程通过操作系统选择哪些线程和进程应该运行、何时运行以及运行多长时间来实现多任务处理。操作在线程和进程之间快速切换,挂起那些未运行的并恢复那些被授予运行时间的。这称为抢占式多任务处理。
Python 中的协程提供了另一种多任务处理类型,称为协作多任务处理。协程是可以挂起和恢复的子例程(函数)。它由 await 表达式暂停,并在 await 表达式解析后恢复。这允许协程通过设计进行合作,选择如何以及何时暂停它们的执行。它是一种替代的、有趣的、强大的并发方法,不同于基于线程和基于进程的并发。仅这一点就可能成为在项目中采用它的理由。协程的另一个关键方面是它们是轻量级的。
它们比线程更轻量级。这意味着它们启动速度更快,使用的内存更少。本质上,协程是一种特殊类型的函数,而线程由 Python 对象表示,并与操作系统中的线程相关联,该对象必须与之交互。因此,我们可能在一个 Python 程序中有数千个线程,但我们很容易在一个线程中拥有数万或数十万个协程。
我们可能会选择协程,因为它们具有可扩展性。
1.2. 使用异步编程
我们可能会选择使用asyncio,因为我们想在我们的程序中使用异步编程。也就是说,我们要开发一个使用异步编程范式的Python程序。异步意味着不同时,与同步或同时相反。在编程时,异步意味着请求动作,尽管在请求时并未执行。它稍后执行。异步编程通常意味着全力以赴并围绕异步函数调用和任务的概念设计程序。虽然还有其他方法可以实现异步编程的元素,但 Python 中的完整异步编程需要使用协程和 asyncio 模块。
我们可能会选择使用 asyncio,因为我们想在我们的程序中使用异步编程模块,这是一个有道理的理由。明确地说,这个原因与使用非阻塞 I/O 无关。异步编程可以独立于非阻塞 I/O 使用。正如我们之前看到的,协程可以异步执行非阻塞 I/O,但是 asyncio 模块还提供了以异步方式执行阻塞 I/O 和 CPU 绑定任务的工具,通过线程在幕后模拟非阻塞和过程。
1.3. 使用非阻塞 I/O
我们可能会选择使用 asyncio,因为我们希望或需要在我们的程序中使用非阻塞 I/O。Input/Output 或简称 I/O 是指从资源读取或写入。
常见的例子包括:
- 硬盘驱动器:读取、写入、追加、重命名、删除等文件。
- 外设:鼠标、键盘、屏幕、打印机、串口、摄像头等。
- 互联网:下载和上传文件、获取网页、查询RSS等。
- 数据库:选择、更新、删除等 SQL 查询。
- 电子邮件:发送邮件、接收邮件、查询收件箱等。
与用 CPU 计算事物相比,这些操作很慢。这些操作在程序中的常见实现方式是发出读或写请求,然后等待发送或接收数据。因此,这些操作通常称为阻塞 I/O 任务。操作系统可以看到调用线程被阻塞,并将上下文切换到另一个将使用 CPU 的线程。这意味着阻塞调用不会减慢整个系统的速度。但它确实会停止或阻塞进行阻塞调用的线程或程序。
非阻塞 I/O 是阻塞 I/O 的替代方案。它需要底层操作系统的支持,就像阻塞 I/O 一样,所有现代操作系统都提供对某种形式的非阻塞 I/O 的支持。非阻塞 I/O 允许读取和写入调用作为异步请求进行。操作系统将处理请求并在结果可用时通知调用程序。
- 非阻塞 I/O:通过异步请求和响应执行 I/O 操作,而不是等待操作完成。
因此,我们可以看到非阻塞 I/O 与异步编程的关系。实际上,我们通过异步编程来使用非阻塞I/O,或者通过异步编程实现非阻塞I/O。
非阻塞 I/O 与异步编程的结合是如此普遍,以至于它通常被简称为异步 I/O。
- 异步 I/O:一种简写,指的是将异步编程与非阻塞 I/O 相结合。
添加 Python 中的 asyncio 模块专门用于向 Python 标准库添加对子进程(例如在操作系统上执行命令)和流(例如 TCP 套接字编程)的非阻塞 I/O 的支持。我们可以使用线程和 Python 线程池或线程池执行器提供的异步编程能力来模拟非阻塞 I/O。asyncio 模块通过协同程序、事件循环和对象来为非阻塞 I/O 提供一流的异步编程,以表示非阻塞子进程和流。
2. 使用 Asyncio 的其他原因
理想情况下,我们会选择一个在项目要求的上下文中得到辩护的理由。有时我们可以控制功能和非功能需求,有时则不能。在我们这样做的情况下,我们可能会出于上述原因之一选择使用 asyncio。在我们不这样做的情况下,我们可能会被引导选择 asyncio 以交付解决特定问题的程序。
我们可能使用 asyncio 的其他一些原因包括:
- 使用 asyncio 是因为其他人为您做出了决定。
- 使用 asyncio,因为你加入的项目已经在使用它。
- 使用 asyncio 是因为您想了解更多有关它的信息。
我们并不总是能够完全控制我们从事的项目。开始一项新工作、新角色或新项目并由直线经理或首席架构师告知特定设计和技术决策是很常见的。
我们可能会在项目上使用 asyncio,因为项目已经在使用它。您必须使用 asyncio,而不是您选择使用 asyncio。我们可能会在项目上使用 asyncio,因为项目已经在使用它。您必须使用 asyncio,而不是您选择使用 asyncio。
一个相关示例可能是您希望采用的使用 asyncio 的问题的解决方案:
- 也许您需要使用第三方 API,并且代码示例使用 asyncio。
- 也许您需要集成一个使用 asyncio 的现有开源解决方案。
- 也许您偶然发现了一些可以满足您需要的代码片段,但它们使用的是 asyncio。
由于缺乏替代解决方案,asyncio 可能会因您选择的解决方案而强加给您。
您可能会选择采用 asyncio 只是因为您想尝试一下,这可能是一个理由。在项目中使用 asyncio 将使它的工作方式具体化。
3. 何时不使用 Asyncio
我们花了很多时间来研究为什么我们应该使用 asyncio。至少花点时间了解为什么我们不应该使用它可能是个好主意。不使用 asyncio 的一个原因是您无法使用上述原因之一来捍卫它的使用。这并非万无一失。可能还有其他使用它的原因,上面没有列出。但是,如果你选择一个使用 asyncio 的理由,而这个理由对你的具体情况来说感觉很薄弱或充满漏洞。也许 asyncio 不是正确的解决方案。我认为不使用 asyncio 的主要原因是它没有提供您认为的好处。
关于 Python 并发性存在许多误解,尤其是围绕 asyncio:
- Asyncio 将围绕全局解释器锁工作。
- Asyncio 比线程更快。
- Asyncio 避免了对互斥锁和其他同步原语的需要。
- Asyncio 比线程更容易使用。
以上都是错误的理解!
按照设计,一次只能运行一个协程,它们协作执行。这就像 GIL 下的线程一样。事实上,GIL 是一个正交问题,在大多数情况下使用 asyncio 时可能无关紧要。任何你可以用 asyncio 编写的程序,你都可以用线程编写,而且它可能会一样快或更快。它也可能更简单,更容易被其他开发人员阅读和解释。您可能会想到线程的任何并发故障模式,您都可能会遇到协程。您必须使协同程序免受死锁和竞争条件的影响,就像线程一样。
不使用 asyncio 的另一个原因是你不喜欢异步编程。
异步编程在许多不同的编程社区中流行了一段时间,最著名的是 JavaScript 社区。它不同于过程式、面向对象和函数式编程,一些开发人员就是不喜欢它。
现在我们已经熟悉了何时使用 asyncio,让我们更详细地了解协程。