python实现异步的⼏种⽅式_终于搞明⽩了,异步Python⽐同
步Python究竟快在哪。。。
⼤家好,你是否听⼈们说过,异步 Python 代码⽐“普通(或同步)Python 代码更快? 果真是那样吗?
同步和异步是什么意思?
Web 应⽤程序通常要处理许多请求,这些请求在短时间内来⾃不同的客户端。为避免处理延迟,必须考虑并⾏处理多个请求,这通常称
为“并发”。
在本⽂中,我将继续使⽤ Web 应⽤程序作为例⼦,但还有其它类型的应⽤程序也从并发中获益。因此,这个讨论并不仅仅是针对 Web 应⽤程序的。
术语“同步”和“异步”指的是编写并发应⽤程序的两种⽅式。所谓的“同步”服务器使⽤底层操作系统⽀持的线程和进程来实现这种并发性。下⾯是同步部署的⼀个⽰意图:
在这种情况下,我们有 5 台客户端,都向应⽤程序发送请求。这个应⽤程序的访问⼊⼝是⼀个 Web 服务器,通过将服务分配给⼀个服务器worker 池来充当负载均衡器,这些 worker 可以实现为进程、线程或者两者的结合。这些 worker 执⾏负载均衡器分配给他们的请求。你使⽤ Web 应⽤程序框架(例如 Flask 或 Django)编写的应⽤程序逻辑运⾏在这些 worker 中。
这种类型的⽅案对于有多个 CPU 的服务器⽐较好,因为你可以将 worker 的数量设置为 CPU 的数量,这样你就能均衡地利⽤你的处理器核⼼,⽽单个 Python 进程由于全局解释器锁(GIL)的限制⽆法实现这⼀点。
在缺点上,上⾯的⽰意图也清楚展⽰了这种⽅案的主要局限。我们有 5 个客户端,却只有 4 个 worker。如果这 5 个客户端在同⼀时间都发送请求,那么负载均衡器会将某⼀个客户端之外的所有请求发送到 worker 池,⽽剩下的请求不得不保留在⼀个队列中,等待有 worker 变得可⽤。
因此,五分之四的请求会⽴即响应,⽽剩下的五分之⼀需要等⼀会⼉。服务器优化的⼀个关键就在于
选择适当数量的 worker 来防⽌或最⼩化给定预期负载的请求阻塞。
⼀个异步服务器的配置很难画,但是我尽⼒⽽为:
这种类型的服务器运⾏在单个进程中,通过循环控制。这个循环是⼀个⾮常有效率的任务管理器和调度器,创建任务来执⾏由客户端发送的请求。与长期存在的服务器 worker 不同,异步任务是由循环创建,⽤来处理某个特定的请求,当那个请求完成时,该任务也会被销毁。任何时候,⼀台异步服务器都会有上百或上千个活跃的任务,它们都在循环的管理下执⾏⾃⼰的⼯作。
你可能想知道异步任务之间的并⾏是如何实现的。这就是有趣的部分,因为⼀个异步应⽤程序通过唯⼀的协同多任务处理来实现这点。这意味着什么?
当⼀个任务需要等待⼀个外部事件(例如,⼀个数据库服务器的响应)时,不会像⼀个同步的 worker 那样等待,⽽是会告诉循环,它需要等待什么,然后将控制权返回给它。循环就能够在这个任务被数据库阻塞的时候发现另外⼀个准备就绪的任务。最终,数据库将发送⼀个响应,⽽那时循环会认为第⼀个的任务已经准备好再次运⾏,并将尽快恢复它。
异步任务暂停和恢复执⾏的这种能⼒可能在抽象上很难理解。为了帮你应⽤到你已经知道的东西,可以考虑在 Python 中使⽤await或yield 关键字这⼀⽅法来实现,但你之后会发现,这并不是唯⼀实现异步任务的⽅法。
⼀个异步应⽤程序完全运⾏在单个进程或线程中,这可以说是令⼈吃惊的。当然,这种类型的并发需要遵循⼀些规则,因此,你不能让⼀个任务占⽤ CPU 太长时间,否则,剩余的任务会被阻塞。为了异步执⾏,所有的任务需要定时主动暂停并将控制权返还给循环。
为了从异步⽅式获益,⼀个应⽤程序需要有经常被 I/O 阻塞的任务,并且没有太多 CPU ⼯作。Web 应⽤程序通常⾮常适合,特别是当它们需要处理⼤量客户端请求时。
在使⽤⼀个异步服务器时,为了最⼤化多 CPU 的利⽤率,通常需要创建⼀个混合⽅案,增加⼀个负载均衡器并在每个 CPU 上运⾏⼀个异步服务器,如下图所⽰:
Python异步的2种⽅法
我敢肯定,你知道要在 Python 中写⼀个异步应⽤程序,你可以使⽤ asyncio package,这个包是在协程的基础上实现了所有异步应⽤程序都需要的暂停和恢复特性。其中yield关键字,以及更新的async和await都是asyncio构建异步能⼒的基础。
/3/library/asyncio.html
Python ⽣态系统中还有其它基于协程的异步⽅案,例如 Trio 和 Curio。还有 Twisted,它是所有协程框架中最古⽼的,甚⾄出现得⽐asyncio都要早。
如果你对编写异步 Web 应⽤程序感兴趣,有许多基于协程的异步框架可以选择,包括 aiohttp、sanic、FastAPI 和 Tornado。
很多⼈不知道的是,协程只是 Python 中编写异步代码的两种⽅法之⼀。第⼆种⽅法是基于⼀个叫做 greenlet 的库,你可以⽤ pip 安装它。Greenlets 和协程类似,它们也允许⼀个 Python 函数暂停执⾏并稍后恢复,但是它们实现这点的⽅式完全不同,这意味着 Python 中的异步⽣态系统分成两⼤类。
协程与 greenlets 之间针对异步开发最有意思的区别是,前者需要 Python 语⾔特定的关键字和特性才能⼯作,⽽后者并不需要。我的意思是,基于协程的应⽤程序需要使⽤⼀种特定的语法来书写,⽽基于 greenlet 的应⽤程序看起来⼏乎和普通 Python 代码⼀样。这⾮常酷,因为在某些情况下,这让同步代码可以被异步执⾏,这是诸如asyncio之类的基于协程的⽅案做不到的。
那么在 greenlet ⽅⾯,跟asyncio对等的库有哪些?我知道 3 个基于 greenlet 的异步包:Gevent、Eventlet 和 Meinheld,尽管最后⼀个更像是⼀个 Web 服务器⽽不是⼀个通⽤的异步库。它们都有⾃⼰的异步循环实现,⽽且它们都提供了⼀个有趣的“monkey-patching”功能,取代了 Python 标准库中的阻塞函数,例如那些执⾏⽹络和线程的函数,并基于 greenlets 实现了等效的⾮阻塞版本。如果你有⼀些同步代码想要异步运⾏,这些包会对你有所帮助。
据我所知,唯⼀明确⽀持greenlet 的 Web 框架只有 Flask。这个框架会⾃动监测,当你想要运⾏在⼀个 greenlet Web 服务器上时,它会⾃我进⾏相应调整,⽽⽆需进⾏任何配置。这么做时,你需要注意不要调⽤阻塞函数,或者,如果你要调⽤阻塞函数,最好⽤猴⼦补丁
来“修复”那些阻塞函数。
但是,Flask 并不是唯⼀受益于 greenlets 的框架。其它 Web 框架,例如 Django 和 Bottle,虽然没有 greenlets,但也可以通过结合⼀个 greenlet Web 服务器并使⽤ monkey-patching修复阻塞函数的⽅式来异步运⾏。
异步⽐同步更快吗?
对于同步和异步应⽤程序的性能,存在着⼀个⼴泛的误解——异步应⽤程序⽐同步应⽤程序快得多。
对此,我需要澄清⼀下。⽆论是⽤同步⽅式写,还是⽤异步⽅式写,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。
结论
希望本⽂能解答异步代码的⼀些困惑和误解。我希望你能记住以下两个关键点:
异步应⽤程序只有在⾼负载下才会⽐同步应⽤程序做得更好
多亏了 greenlets,即使你⽤⼀般⽅式写代码并使⽤ Flask 或 Django 之类的传统框架,也能从异步中受益。