你是否经常听人们说,异步Python代码比普通(或同步)Python代码更快?果真是那样吗?
“同步”和“异步”是什么意思?
Web应用程序通常要处理许多请求,这些请求在很短的时间段内来自不同的客户端。为避免处理延迟,必须考虑并行处理多个请求,这通常称为“并发”(concurrency)。
在本文中,我将继续使用Web应用程序作为例子,但是要记住还有其它类型的应用程序也从并发完成多个任务中获益,因此这个讨论并不仅仅是针对Web应用程序的。
术语“同步”(sync)和“异步”(async)指的是编写并发应用程序的两种方式。所谓的“同步”服务器使用底层操作系统支持的线程和进程来实现这种并发性。下面是同步部署的一个示意图:
在这种情况下,我们有5台客户端,都向应用程序发送请求。这个应用程序的访问入口是一个Web服务器,通过负载均衡将请求分配给多个服务器worker处理请求,这些worker可以实现多进程、多线程或者两者的结合。你使用Web应用程序框架(例如Flask或Django)编写的应用程序逻辑运行在这些worker中。
这种类型的方案对于有多个CPU的服务器比较好,因为你可以将worker的数量设置为CPU的数量,这样你就能均衡地利用你的处理器核心,而单个Python进程由于全局解释器锁(GIL)的限制无法实现这一点。
在缺点方面,上面的示意图也清楚展示了这种方案的主要局限。我们有5个客户端,却只有4个worker。如果这5个客户端在同一时间都发送请求,那么负载均衡器会将某一个客户端之外的所有请求发送到worker池,而剩下的请求不得不保留在一个队列中,等待有worker变得可用位置。因此,五分之四的请求会立即响应,而剩下的五分之一需要等一会儿。服务器优化的一个关键就在于选择适当数量的worker来防止或最小化给定预期负载的请求阻塞。
一个异步服务器的配置很难画,但是我会尽力而为:
这种类型的服务器运行在单个进程中,通过循环(loop)控制。这个loop是一个非常有效率的任务管理器和调度器,创建任务来执行由客户端发送的请求。与长期存在的服务器worker不同,异步任务是由循环创建,用来处理某个特定的请求,当那个请求完成时,该任务也会被销毁。任何时候,一台异步服务器都会有上百或上千个活跃的任务,它们都在循环的管理下执行自己的工作。
你可能想知道异步任务之间的并行是如何实现的。这就是有趣的部分,因为一个异步应用程序通过唯一的协同多任务(cooperativemulti-tasking)处理来实现这点。这意味着什么?当一个任务需要等待一个外部事件(例如,一个数据库服务器的响应)时,不会像一个同步的worker那样等待,而是会告诉循环它需要等待什么,然后将控制权返回给它。循环就能够在这个任务被数据库阻塞的时候发现另外一个准备就绪的任务。最终,数据库将发送一个响应,而那时循环会认为第一个的任务已经准备好再次运行,并将尽快恢复它。
异步任务暂停和恢复执行的这种能力可能在抽象上很难理解。为了帮助你应用到你已经知道的东西,可以考虑在Python中使用await或yield关键字这一方法来实现,但你之后会发现这并不是唯一实现异步任务的方法。
一个异步应用程序完全运行在单个进程或线程中,这可以说是令人吃惊的。当然,这种类型的并发需要遵循一些规则,因此你不能让一个任务占用CPU太长时间,否则剩余的任务会被阻塞。为了异步执行,所有的任务需要定时主动暂停并将控制权返还给循环。为了从异步方式获益,一个应用程序需要有经常被I/O阻塞的任务,并且没有太多CPU工作。Web应用程序通常非常适合,特别是当它们需要处理大量客户端请求时。
在使用一个异步服务器时,为了最大化多CPU的利用率,通常需要创建一个混合方案,增加一个负载均衡器并在每个CPU上运行一个异步服务器,如下图所示:
Python中实现异步的2种方法
我敢肯定,你知道要在Python中写一个异步应用程序,你可以使用asyncio这个库,这个包是在协程(coroutine)的基础上实现了所有异步应用程序都需要的暂停和恢复特性。其中yield关键字,以及更新的async和await都是asyncio构建异步能力的基础。
Python生态系统中还有其它基于协程的异步方案,例如Trio和Curio。还有Twisted,它是所有协程框架中最古老的,甚至出现得比asyncio都要早。
如果你对编写异步Web应用程序感兴趣,有许多基于协程的异步框架可以选择,包括aio