翻译
Sync vs. Async Python: What is the Difference?
你有没有听人说过异步 Python 代码比“一般”(或同步) Python 代码更快?这怎么可能?在本文中,我将尝试解释什么是异步以及它与一般 Python 代码的区别。
Sync 和 Async 是什么意思?
Web 应用程序通常须要解决许多申请,所有申请都是在短时间内从不同的客户端收回的。为了防止解决提早,它们必须可能并行处理多个申请(通常称为并发)。在本文中,我将持续应用 web 应用程序作为例子,但请记住,还有其余类型的应用程序也受害于同时实现多个工作,因而这个探讨并不是专门针对 web 的。
术语“sync”和“async”指的是编写应用并发的应用程序的两种形式。所谓的“sync”服务器应用线程和过程的底层操作系统反对来实现这种并发。以下是同步部署的效果图:
在这种状况下,咱们有五个客户端,所有客户端都向应用程序发送申请。这个应用程序的公共拜访点是一个 web 服务器,它充当一个负载均衡器,将申请分发给一组服务器 worker,这些 worker 能够实现为过程、线程或两者的组合。worker 执行负载均衡器调配给他们的申请。你能够用 Flask 或 Django 这样的 web 应用程序框架来编写利用程序逻辑,它们就位于这些 worker 中。
这种类型的解决方案非常适合领有多个 CPU 的服务器,因为你能够将 worker 的数量配置为 cpu 数量的倍数,并且通过这种配置,实现 cores 的平均利用率,这是单个 Python 过程无奈做到的,因为 全局解释器锁定(GIL)强加了一些限度。
就毛病而言,上图分明地表明了此办法的次要局限性。咱们有 5 个客户端,但只有 4 个 worker。如果这 5 个客户端同时发送申请,而负载平衡器只能给每个 worker 分派 1 个申请,没有竞争到 worker 的申请将保留在队列中,期待一个 worker 可用。因而,5 个客户端中有 4 个将及时收到回复,但其中 1 个将不得不期待更长的工夫。使服务器性能良好的关键在于抉择适当数量的 worker,以避免或尽量减少在给定预期负载的状况下阻塞申请的状况。
异步服务器设置较难绘制,但这是我的最佳抉择:
这种类型的服务器在由循环管制的单个过程中运行。循环是一个十分高效的工作管理器和调度器,它创立工作以解决客户端发送的申请。与长期运行的服务器 worker 不同,循环会创立一个异步工作来解决特定的申请,当该申请实现时,该工作将被销毁。在任何给定的工夫,异步服务器可能有数百个甚至数千个流动工作,所有这些工作由循环治理并同时实现本人的工作。
你可能想晓得异步工作之间的并行性是如何实现的。这是乏味的局部,因为异步应用程序齐全依赖于合作式多任务处理。这意味着什么?当一个工作须要期待一个内部事件时,例如来自数据库服务器的响应,而不是像同步 worker 那样期待,它通知循环须要期待什么,而后将控制权返回给循环。而后循环能够找到另一个筹备运行的工作,而此工作被数据库阻塞。最终数据库将发送响应,此时循环将思考第一个工作筹备再次运行,并将尽快恢复它。
异步工作暂停和复原执行的这种能力比拟形象可能难以了解。为了帮忙你将其利用于你可能曾经晓得的事件,请思考在 Python 中,实现此目标的一种办法是应用 await
或 yield
关键字,但这并不是惟一的办法,你稍后将看到。
异步应用程序齐全在单个过程和单个线程中运行,这令人诧异。当然,这种类型的并发须要肯定的规定,因为你 不能让工作在 CPU 上停留太长时间,否则残余的工作就会饿死 。为了使异步工作,所有工作都须要主动暂停并及时将控制权返回给循环。要从异步格调中受害,应用程序须要执行的工作 通常会被 I/O 阻塞,并且不须要太多的 CPU 工作。Web 应用程序通常非常适合,特地是如果它们须要解决大量客户端申请时。
为了在应用异步服务器时最大限度地利用多个 CPU,通常会创立一个混合解决方案,增加一个负载平衡器并在每个 CPU 上运行一个异步服务器,如下图所示:
在 Python 中实现异步的两种办法
我确定你晓得,要在 Python 中编写异步应用程序,你能够应用 asyncio 包,它构建在协程之上,以实现所有异步应用程序都须要的挂起和复原个性。关键词 yield
,以及更新的 async
和 await
,是 asyncio 构建异步性能的根底。为了描述一幅残缺的图景,Python 生态系统中还有其余基于协程的异步解决方案,比方 Trio 和 Curio。还有 Twisted,它是最古老的协同框架,甚至早于 asyncio
。
如果你有趣味编写一个异步 web 应用程序,有很多基于 coroutines 的异步框架可供选择,包含 aiohttp、sanic、FastAPI 和 Tornado。
很多人不晓得的是,协程只是 Python 中可用于编写异步代码的两种办法之一。第二种办法是基于一个名为 greenlet 的软件包,你能够应用 pip 进行装置。Greenlets 与协程相似,因为它们也容许 Python 函数暂停执行并在当前复原执行,然而实现形式却齐全不同,这意味着 Python 中的异步生态系统分为两个大类。
coroutine 和 greenlets 进行异步开发的乏味的区别在于,前者须要 Python 语言的特定关键字和个性能力工作,而后者不须要。我的意思是,基于 coroutine 的应用程序须要应用十分特定的语法编写,而基于 greenlet 的应用程序看起来与一般的 Python 代码齐全一样。这十分酷,因为在某些条件下,它容许异步执行同步代码,这是基于 coroutine 的解决方案(例如 asyncio)无奈做到的。
那么在 greenlet 方面有哪些 asyncio
的等价物呢?我晓得三个基于 greenlets 的异步包: Gevent, Eventlet 和 Meinheld,只管最初一个更像是一个 web 服务器而不是一个通用的异步库。它们都有本人的异步循环实现,并且提供了一个乏味的“monkey-patching”个性,用在 greenlets 上实现的等效非阻塞版本替换 Python 规范库中的阻塞函数,比方那些执行网络和线程的函数。如果你有一段心愿异步运行的同步代码,那么这些包很有可能会让你做到这一点。
你会对此感到诧异的。据我所知,惟一明确反对 greenlets 的 web 框架是 Flask。此框架会自动检测你何时在 greenlet web 服务器上运行,并进行相应的调整,而无需进行任何配置。在执行此操作时,您须要留神不要调用阻塞函数,否则,请应用 Monkey-patching 来“修复”这些阻塞函数。
然而,Flask 并不是惟一能够从 greenlets 中受害的框架。其余的 web 框架,比方 Django 和 Bottle,它们不晓得 greenlets,当它们与 greenlet web 服务器配对时,也能够异步运行,并且猴子补丁修复了阻塞性能。
异步比同步更快吗?
对于同步和异步应用程序的性能,存在宽泛的误会。人们认为异步应用程序比同步应用程序要快得多。
让我廓清一下,以便咱们达成对立认知。不论 Python 代码是同步编写还是异步编写,它的运行速度都是完全相同的。除了代码外,还有两个因素能够影响并发应用程序的性能:上下文切换和可伸缩性。
上下文切换
在所有正在运行的工作之间偏心地共享 CPU 所需的工作(称为上下文切换)可能会影响应用程序的性能。对于同步应用程序,此工作由操作系统实现 ,并且基本上是一个没有配置或微调选项的黑匣子。 对于异步应用程序,上下文切换由循环实现。
asyncio 提供的默认循环实现是用 Python 编写的,它不被认为是十分高效的。uvloop
软件包提供了一个代替循环,该循环局部用 C 代码实现,以实现更好的性能。Gevent
和 Meinheld
应用的事件循环也用 C 代码编写。Eventlet
应用 Python 编写的循环。
高度优化的异步循环在进行上下文切换方面可能比操作系统更无效,然而以我的教训,要想看到切实的性能晋升,你必须在很高的并发级别上运行。对于大多数应用程序,我认为同步和异步上下文切换之间的性能差别不会很显著。
伸缩性
我认为,异步更快的神话来源于异步应用程序通常可能更无效地应用 cpu,因为它们比同步具备更好的伸缩性和更灵便的办法。
考虑一下如果上图所示的同步服务器同时接管 100 个申请,将会产生什么状况。该服务器一次不能解决超过 4 个申请,因而其中大多数申请将在队列中期待一段时间,而后能力调配 worker。
与异步服务器相比,异步服务器会立刻创立 100 个工作(如果应用混合模型,4 个异步 worker 每个会创立 25 个工作)。应用异步服务器,所有申请都能够在不用期待的状况下开始解决(不过偏心地说,可能还存在其余会升高速度的瓶颈,比方对流动数据库连接数的限度)。
如果这 100 个工作大量应用 CPU,那么同步和异步解决方案将有相似的性能,因为 CPU 运行的速度是固定的,Python 的执行代码的速度总是雷同的,应用程序所做的工作也是雷同的。然而,如果工作须要执行大量 I/O 操作,那么只有 4 个并发申请,同步服务器可能无奈实现高 CPU 利用率。另一方面,异步服务器必定可能更好地放弃 cpu 处于繁忙状态,因为它并行地运行所有 100 个申请。
你可能想晓得为什么不能运行 100 个同步 worker,这样两个服务器就具备雷同的并发性。考虑一下,每个 worker 都须要有本人的 Python 解释器,以及与之相干的所有资源,再加上具备本人资源应用程序的一个独自正本。服务器和应用程序的大小将决定能够运行多少个 worker 实例,但通常这个数字并不是很高。另一方面,异步工作十分轻量级,并且都在单个 worker 过程的上下文中运行,因而它们具备显著的劣势。
记住这些,咱们能够说异步只有在以下状况下才会比同步更快:
- 高负载(没有高负载就没有高并发性的劣势)
- 工作受 I/O 束缚(如果工作受 CPU 束缚,那么 超过 CPU 数以上的并发性就没有帮忙了)
- 你能够查看 每单位工夫解决的均匀申请数。如果查看单个申请解决工夫,你不会看到很大的差别,并且因为有更多的并发工作竞争 CPU,异步甚至可能会略微慢一点
我心愿本文可能廓清一些对于异步代码的混同和误会。我心愿你们记住以下两个要点:
- 在高负载下,异步应用程序只会比同步等效程序做得更好
- 因为 greenlet,即便你编写一般代码并应用传统框架(如 Flask 或 Django),也能够从异步中受害
如果你想更具体地理解异步零碎是如何工作的,请查看我的 PyCon 演示文稿 Asynchronous Python for the Complete Beginner。