你听人说过异步Python代码比普通(或同步)Python代码更快吗?怎么会这样呢?在这篇文章中,我将尝试解释异步是什么,以及它与普通Python代码有何不同。
Web应用程序通常必须处理许多请求,所有这些请求都来自不同的客户端,并且都是在短时间内到达的。为了避免处理延迟,它们必须能够并行处理多个请求,这通常称为并发性。在本文中,我将继续使用Web应用程序作为示例,但请记住,还有其他类型的应用程序也可以从同时完成多个任务中获益,因此本讨论并不局限于Web。
术语同步和异步指的是编写使用并发的应用程序的两种方式。所谓的同步服务器使用底层操作系统对线程和进程的支持来实现这种并发性。以下是同步部署可能的外观示意图:
在这种情况下,我们有五个客户端,所有客户端都向应用程序发送请求。此应用程序的公共访问点是一台Web服务器,它通过在服务器工作进程池中分发请求来充当负载平衡器,服务器工作进程池可以实现为进程、线程或两者的组合。工作进程按照负载平衡器分配给它们的方式执行请求。您可以使用Flask或Django等Web应用程序框架编写的应用程序逻辑驻留在这些工作器中。
这种类型的解决方案对于具有多个CPU的服务器非常有用,因为您可以将工作进程的数量配置为CPU数量的倍数,这样就可以实现内核的均匀利用,这是单个Python进程由于全局解释器锁(Global Interpreter Lock,GIL)的限制而无法做到的。
就缺点而言,上图清楚地显示了此方法的主要限制是什么。我们有五个客户,但只有四个员工。如果这五个客户端同时发送它们的请求,那么负载平衡器将能够将除一个请求之外的所有请求分派给工作进程,并且在等待工作进程变得可用期间,输掉竞争的请求将不得不保留在队列中。因此,五个客户中有四个将及时收到回复,但其中一个将不得不等待更长时间。使服务器性能良好的关键是在给定预期负载的情况下选择适当数量的工作进程来防止或最小化阻塞的请求。
此类型的服务器在由循环控制的单个进程中运行。循环是一个非常高效的任务管理器和调度器,它创建任务来执行客户端发送的请求。与长期存在的服务器工作器不同,异步任务由循环创建以处理特定请求,当该请求完成时,任务将被销毁。在任何给定时间,异步服务器都可能有数百甚至数千个活动任务,所有任务都在做自己的工作,同时由循环管理。
您可能想知道异步任务之间的并行性是如何实现的。这是有趣的部分,因为异步应用程序完全依赖于协作多任务来实现这一点。这是什么意思?当任务需要等待外部事件(例如,来自数据库服务器的响应)时,它不会像同步工作者那样只等待,而是告诉循环它需要等待什么,然后将控制权返回给它。然后,当另一个任务被数据库阻塞时,循环能够找到准备运行的另一个任务。最终,数据库将发送响应,此时循环将认为第一个任务已准备好再次运行,并将尽快恢复该任务。
异步任务挂起和恢复执行的这种能力在抽象上可能很难理解。为了帮助您将这一点应用到您可能已经知道的事情上,考虑一下在Python中,实现这一点的一种方法是使用AWAIT或YILE关键字,但正如您稍后将看到的那样,这些并不是唯一的方法。
异步应用程序完全在单个进程和单个线程中运行,这简直令人惊叹。当然,这种类型的并发需要一些规则,因为您不能让一个任务长时间占用CPU,否则剩余的任务就会饿死。要让异步运行,所有任务都需要主动暂停,并及时将控制权归还给循环。要从异步样式中获益,应用程序需要有经常被I/O阻塞的任务,并且不要有太多的CPU工作。Web应用程序通常非常适合,特别是当它们需要处理大量客户端请求时。
要在使用异步服务器时最大限度地利用多个CPU,通常会创建一个混合解决方案,在每个CPU上添加一个负载均衡器并运行一个异步服务器,如下图所示:
我相信您知道,要用Python编写异步应用程序,可以使用Asyncio包,它构建在协程之上,以实现所有异步应用程序都需要的挂起和恢复功能。Year关键字以及较新的Async和AWAIT是构建Asyncio异步功能的基础。为了描绘一幅完整的图景,在Python生态系统中还有其他基于协程的异步解决方案,如Trio和Curio。还有Twisted,它是最古老的协程框架,甚至早于Asyncio。
如果您对编写异步web应用程序感兴趣,有许多基于协程的异步框架可供选择,包括aiohttp、sanic、FastAPI和Tornado。
很多人不知道的是,协程只是Python中可用来编写异步代码的两种方法之一。第二种方法是基于一个名为greenlet的包,您可以使用pip安装该包。Greenlet与协程的相似之处在于,它们还允许Python函数暂停执行并在稍后恢复执行,但它们实现这一点的方式完全不同,这意味着Python中的异步生态系统被分成两大组。
异步开发的协例程和greenlet之间的有趣区别在于,前者需要特定的关键字和Python语言的特性才能工作,而后者则不需要。我的意思是,基于协程的应用程序需要使用非常特定的语法编写,而基于greenlet的应用程序看起来完全像普通的Python代码。这非常酷,因为在某些条件下,它允许异步执行同步代码,这是基于协程的解决方案(如Asyncio)无法做到的。
那么,在绿地方面,相当于异步运动的是什么呢?我知道有三个基于greenlet的异步软件包:Gevent、Eventlet和Meinhold,不过最后一个更像是一个Web服务器,而不是一个通用的异步库。它们都有自己的异步循环实现,并且它们提供了一个有趣的猴子修补功能,用在greenlet之上实现的等效非阻塞版本替换了Python标准库中的阻塞函数,比如那些执行联网和线程化的阻塞函数。如果您有一段想要异步运行的同步代码,那么这些包很有可能会让您这样做。
你会对此感到惊讶的。据我所知,唯一明确支持greenlet的web框架不是别人,就是Flask。当您在greenlet web服务器上运行时,此框架会自动检测并相应地进行调整,无需任何配置。在这样做时,您需要注意不要调用阻塞函数,或者如果您调用了阻塞函数,则使用猴子修补来修复这些阻塞函数(";Fix";First";)。
但是,Flask并不是唯一可以从Greenlet中获益的框架。其他web框架,如Django和Bottle,不了解greenlet,当与greenlet web服务器配合使用时,也可以异步运行,并且阻塞功能也是打了补丁的。
关于同步和异步应用程序的性能存在广泛传播的误解。人们相信异步应用程序比它们的同步应用程序要快得多。
让我澄清这一点,以便我们都站在同一立场上。无论Python代码是以同步还是异步方式编写,其运行速度都完全相同。除了代码之外,还有两个因素会影响并发应用程序的性能:上下文切换和可伸缩性。
在所有正在运行的任务之间公平共享CPU所需的工作称为上下文切换,这可能会影响应用程序的性能。在同步应用程序的情况下,这项工作由操作系统完成,基本上是一个没有配置或微调选项的黑匣子。对于异步应用,上下文切换由循环完成。
Asyncio提供的默认循环实现(用Python编写)被认为效率不高。Uvloop包提供了部分用C代码实现的替代循环,以实现更好的性能。Gevent和Meinhold使用的事件循环也是用C代码编写的。Eventlet使用用Python编写的循环。
高度优化的异步循环在执行上下文切换方面可能比操作系统更高效,但根据我的经验,要想看到明显的性能提升,您必须在非常高的并发级别上运行。对于大多数应用程序,我认为同步和异步上下文切换之间的性能差异并不显著。
我认为异步速度更快的神话的来源是,异步应用程序通常会更有效地使用CPU,因为它们的扩展能力比同步要好得多,而且以一种比同步更灵活的方式。
考虑上图中所示的同步服务器如果同时接收100个请求会发生什么情况。此服务器一次不能处理四个以上的请求,因此这些请求中的大多数将在队列中等待一段时间,然后才能分配工作器。
与异步服务器形成对比的是,异步服务器将立即创建100个任务(如果使用混合模型,则四个异步工作器中的每个工作器将创建25个任务)。使用异步服务器,所有请求都无需等待即可开始处理(尽管公平地说,未来可能会有其他瓶颈减慢速度,例如对活动数据库连接数量的限制)。
如果这一百个任务大量使用CPU,那么同步和异步解决方案将具有类似的性能,因为CPU运行的速度是固定的,Python执行代码的速度总是相同的,应用程序要做的工作也是相等的。但是,如果任务需要执行大量I/O操作,则同步服务器可能无法在仅有4个并发请求的情况下实现高CPU利用率。另一方面,异步服务器在保持CPU繁忙方面肯定会更好,因为它并行运行所有100个请求。
您可能会想,为什么不能运行100个同步工作进程,从而使两台服务器具有相同的并发性。考虑到每个工作者都需要有自己的Python解释器和与之相关的所有资源,再加上一个单独的应用程序副本和自己的资源。您的服务器和应用程序的大小将决定您可以运行的工作实例的数量,但通常这个数字不会很高。另一方面,异步任务是极其轻量级的,并且都在单个工作进程的上下文中运行,因此它们具有明显的优势。
考虑到所有这些,我们可以说,对于给定方案,异步可能比同步更快,只有在以下情况下:
存在高负载(没有高负载,访问高并发没有优势)。
任务受I/O限制(如果任务受CPU限制,则超过CPU数量的并发性无济于事)。
您可以查看每单位时间处理的平均请求数。如果您查看各个请求的处理时间,您将不会看到很大的差异,异步甚至可能会稍微慢一些,因为有更多的并发任务竞争CPU。
我希望这篇文章能澄清一些关于异步代码的困惑和误解。我希望您记住的两个重要要点是:
多亏了Greenlet,即使您编写普通代码并使用Flask或Django等传统框架,也可以从异步中获益。
如果您想更详细地了解异步系统是如何工作的,请在YouTube上查看我的PyCon演示文稿“完整初学者的异步Python”。
关于同步和异步之间的区别,您有什么挥之不去的问题吗?请在下面的评论中让我知道!
您好,感谢您访问我的博客!如果你喜欢这篇文章,请考虑支持我在Patreon这个博客上的工作!
你能举一个猴子修补阻塞函数的例子吗?可能是一个很好的指针,教你如何在烧瓶里贴猴子。
@Srikar:您需要查看您正在使用的异步库的文档,看看如何进行这种猴子修补。您只需要调用它们提供的函数。
每当我必须使用Asyncio或Node.js时,我都要感谢我把大部分工作时间都花在了使用Erlang/Elixir上。