.

技术经验深入理解Python异步编程

●小编推荐

本篇文章为读者深入讲解Python异步编程相关的知识,解决在使用异步编程中的问题和疑惑,享受Python带来的简洁优雅和高效率。

原创:阿驹责编:啊Q

前言

很多朋友对异步编程都处于“听说很强大”的认知状态。鲜有在生产项目中使用它。而使用它的同学,则大多数都停留在知道如何使用Tornado、Twisted、Gevent这类异步框架上,出现各种古怪的问题难以解决。而且使用了异步框架的部分同学,由于用法不对,感觉它并没牛逼到哪里去,所以很多同学做Web后端服务时还是采用Flask、Django等传统的非异步框架。

从上两届PyCon技术大会看来,异步编程已经成了Python生态下一阶段的主旋律。如新兴的Go、Rust、Elixir等编程语言都将其支持异步和高并发作为主要“卖点”,技术变化趋势如此。Python生态为不落人后,从年起由Python之父Guido亲自操刀主持了Tulip(asyncio)项目的开发。

本系列教程分为上中下篇,让读者深入理解Python异步编程,解决在使用异步编程中的疑惑,深入学习Python3中新增的asyncio库和async/await语法,尽情享受Python带来的简洁优雅和高效率。

内容安排上篇

了解异步编程及其紧密相关的概念,如阻塞/非阻塞、同步/异步、并发/并行等

理解异步编程是什么,以及异步编程的困难之处

理解为什么需要异步编程

熟悉如何从同步阻塞发展到异步非阻塞的

掌握epoll+Callback+Eventloop是如何工作的

掌握Python是如何逐步从回调到生成器再到原生协程以支持异步编程的

掌握asyncio的工作原理

中篇

掌握asyncio标准库基本使用

掌握asyncio的事件循环

掌握协程与任务如何使用与管理(如调度与取消调度)

掌握同步原语的使用(Lock、Event、Condition、Queue)

掌握asyncio和多进程、多线程结合使用

下篇

理解GIL对异步编程的影响

理解asyncio踩坑经验

理解回调、协程、绿程(Green-Thread)、线程对比总结

掌握多进程、多线程、协程各自的适用场景

了解Gevent/libev、uvloop/libuv与asyncio的区别和联系

掌握Python异步编程的一些指导细则

1什么是异步编程

通过学习相关概念,我们逐步解释异步编程是什么。

1.1阻塞

程序未得到所需计算资源时被挂起的状态。

程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。

常见的阻塞形式有:网络I/O阻塞、磁盘I/O阻塞、用户输入阻塞等。

阻塞是无处不在的,包括CPU切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。(如果是多核CPU则正在执行上下文切换操作的核不可被利用。)

1.2非阻塞

程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。

非阻塞并不是在任何程序级别、任何情况下都可以存在的。

仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。

非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。

1.3同步

不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,称这些程序单元是同步执行的。

例如购物系统中更新商品库存,需要用“行锁”作为通信信号,让不同的更新请求强制排队顺序执行,那更新库存的操作是同步的。

简言之,同步意味着有序。

1.4异步

为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式。

不相关的程序单元之间可以是异步的。

例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。

简言之,异步意味着无序。

上文提到的“通信方式”通常是指异步和并发编程提供的同步原语,如信号量、锁、同步队列等等。我们需知道,虽然这些通信方式是为了让多个程序在一定条件下同步执行,但正因为是异步的存在,才需要这些通信方式。如果所有程序都是按序执行,其本身就是同步的,又何需这些同步信号呢?

1.5并发

并发描述的是程序的组织结构。指程序要被设计成多个可独立执行的子任务。

以利用有限的计算机资源使多个任务可以被实时或近实时执行为目的。

1.6并行

并行描述的是程序的执行状态。指多个任务同时被执行。

以利用富余计算资源(多核CPU)加速完成多个任务为目的。

并发提供了一种程序组织结构方式,让问题的解决方案可以并行执行,但并行执行不是必须的。

1.7概念总结

并行是为了利用多核加速多任务完成的进度

并发是为了让独立的子任务都有机会被尽快执行,但不一定能加速整体进度

非阻塞是为了提高程序整体执行效率

异步是高效地组织非阻塞任务的方式

要支持并发,必须拆分为多任务,不同任务相对而言才有阻塞/非阻塞、同步/异步。所以,并发、异步、非阻塞三个词总是如影随形。

1.8异步编程

以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。

如果在某程序的运行时,能根据已经执行的指令准确判断它接下来要进行哪个具体操作,那它是同步程序,反之则为异步程序。(无序与有序的区别)

同步/异步、阻塞/非阻塞并非水火不容,要看讨论的程序所处的封装级别。例如购物程序在处理多个用户的浏览请求可以是异步的,而更新库存时必须是同步的。

1.9异步之难(nán)

控制不住“计几”写的程序,因为其执行顺序不可预料,当下正要发生什么事件不可预料。在并行情况下更为复杂和艰难。

所以,几乎所有的异步框架都将异步编程模型简化:一次只允许处理一个事件。故而有关异步的讨论几乎都集中在了单线程内。

如果某事件处理程序需要长时间执行,所有其他部分都会被阻塞。

所以,一旦采取异步编程,每个异步调用必须“足够小”,不能耗时太久。如何拆分异步任务成了难题。

程序下一步行为往往依赖上一步执行结果,如何知晓上次异步调用已完成并获取结果?

回调(Callback)成了必然选择。那又需要面临“回调地狱”的折磨。

同步代码改为异步代码,必然破坏代码结构。

解决问题的逻辑也要转变,不再是一条路走到黑,需要精心安排异步任务。

2苦心异步为哪般

如上文所述,异步编程面临诸多难点,Python之父亲自上阵打磨4年才使asyncio模块在Python3.6中“转正”,如此苦心为什么?答案只有一个:它值得!下面我们看看为何而值得。

2.1CPU的时间观

我们将一个2.6GHz的CPU拟人化,假设它执行一条命令的时间,它感觉上过了一秒钟。CPU是计算机的处理核心,也是最宝贵的资源,如果有浪费CPU的运行时间,导致其利用率不足,那程序效率必然低下(因为实际上有资源可以使效率更高)。

如上图所示,在千兆网上传输2KB数据,CPU感觉过了14个小时,如果是在10M的公网上呢?那效率会低百倍!如果在这么长的一段时间内,CPU只是傻等结果而不能去干其他事情,是不是在浪费CPU的青春?

鲁迅说,浪费“CPU”的时间等于谋财害命。而凶手就是程序猿。

2.2面临的问题

成本问题

如果一个程序不能有效利用一台计算机资源,那必然需要更多的计算机通过运行更多的程序实例来弥补需求缺口。例如我前不久主导重写的项目,使用Python异步编程,改版后由原来的7台服务器削减至3台,成本骤降57%。一台AWSm4.xlarge型通用服务器按需付费实例一年价格约1.2万人民币。

效率问题

如果不在乎钱的消耗,那也会在意效率问题。当服务器数量堆叠到一定规模后,如果不改进软件架构和实现,加机器是徒劳,而且运维成本会骤然增加。比如别人家的电商平台支持单/秒支付,而自家在下单量才支撑单/秒,在双十一这种活动的时候,钱送上门也赚不到。

C10k/C10M挑战

C10k(concurrentlyhandling10kconnections)是一个在年被提出来的技术挑战,如何在一颗1GHzCPU,2G内存,1gbps网络环境下,让单台服务器同时为1万个客户端提供FTP服务。而到了年后,随着硬件技术的发展,这个问题被延伸为C10M,即如何利用8核心CPU,64G内存,在10gbps的网络上保持万并发连接,或是每秒钟处理万的连接。(两种类型的计算机资源在各自的时代都约为美元)

成本和效率问题是从企业经营角度讲,C10k/C10M问题则是从技术角度出发挑战软硬件极限。C10k/C10M问题得解,成本问题和效率问题迎刃而解。

2.3解决方案

《约束理论与企业优化》中指出:“除了瓶颈之外,任何改进都是幻觉。”

CPU告诉我们,它自己很快,而上下文切换慢、内存读数据慢、磁盘寻址与取数据慢、网络传输慢……总之,离开CPU后的一切,除了一级高速缓存,都很慢。我们观察计算机的组成可以知道,主要由运算器、控制器、存储器、输入设备、输出设备五部分组成。运算器和控制器主要集成在CPU中,除此之外全是I/O,包括读写内存、读写磁盘、读写网卡全都是I/O。I/O成了最大的瓶颈。

异步程序可以提高效率,而最大的瓶颈在I/O,业界诞生的解决方案没出意料:异步I/O吧,异步I/O吧,异步I/O吧吧!

3异步I/O进化之路

如今,地球上最发达、规模最庞大的计算机程序,莫过于因特网。而从CPU的时间观中可知,网络I/O是最大的I/O瓶颈,除了宕机没有比它更慢的。所以,诸多异步框架都对准的是网络I/O。

我们从一个爬虫例子说起,从因特网上下载10篇网页。

3.1同步阻塞方式

最容易想到的解决方案就是依次下载,从建立socket连接到发送网络请求再到读取响应数据,顺序进行。

注:总体耗时约为4.5秒。(因网络波动每次测试结果有所变动,本文取多次平均值)

如上图所示,blocking_way()的作用是建立socket连接,发送HTTP请求,然后从socket读取HTTP响应并返回数据。示例中我们请求了example.


转载请注明:http://www.abachildren.com/xgyy/416.html