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)成了必然选择。那又需要面临“回调地狱”的折磨。

3 异步I/O进化之路

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

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

3.1 同步阻塞方式

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

注:总体耗时约为4.5秒。

我们知道,创建网络连接,多久能创建完成不是客户端决定的,而是由网络状况和服务端处理能力共同决定。服务端什么时候返回了响应数据并被客户端接收到可供程序读取,也是不可预测的。所以sock.connect()和sock.recv()这两个调用在默认情况下是阻塞的。

注:sock.send()函数并不会阻塞太久,它只负责将请求数据拷贝到TCP/IP协议栈的系统缓冲区中就返回,并不等待服务端返回的应答确认。

3.2 改进方式:多进程

在一个程序内,依次执行10次太耗时,那开10个一样的程序同时执行不就行了。于是我们想到了多进程编程。为什么会先想到多进程呢?发展脉络如此。在更早的操作系统(Linux 2.4)及其以前,进程是 OS 调度任务的实体,是面向进程设计的OS。

注:总体耗时约为 0.6 秒。

改善效果立竿见影。但仍然有问题。总体耗时并没有缩减到原来的十分之一,而是九分之一左右,还有一些时间耗到哪里去了?进程切换开销。

当进程数量大于CPU核心数量时,进程切换是必然需要的。

除了切换开销,一般的服务器在能够稳定运行的前提下,可以同时处理的进程数在数十个到数百个规模。如果进程数量规模更大,系统运行将不稳定,而且可用内存资源往往也会不足。

多进程解决方案在面临每天需要成百上千万次下载任务的爬虫系统,或者需要同时搞定数万并发的电商系统来说,并不适合。

除了切换开销大,以及可支持的任务规模小之外,多进程还有其他缺点,如状态共享等问题。

3.3 继续改进:多线程

由于线程的数据结构比进程更轻量级,同一个进程可以容纳多个线程,从进程到线程的优化由此展开。后来的OS也把调度单位由进程转为线程,进程只作为线程的容器,用于管理进程所需的资源。而且OS级别的线程是可以被分配到不同的CPU核心同时运行的。

注:总体运行时间约0.43秒。

结果符合预期,比多进程耗时要少些。从运行时间上看,多线程似乎已经解决了切换开销大的问题。而且可支持的任务数量规模,也变成了数百个到数千个。

但是,多线程仍有问题,特别是Python里的多线程。首先,Python中的多线程因为GIL的存在,它们并不能利用CPU多核优势,一个Python进程中,只允许有一个线程处于运行状态。那为什么结果还是如预期,耗时缩减到了十分之一?

因为在做阻塞的系统调用时,例如sock.connect(),sock.recv()时,当前线程会释放GIL,让别的线程有执行机会。但是单个线程内,在阻塞调用上还是阻塞的。

另外,线程是被OS调度,调度策略是抢占式的,以保证同等优先级的线程都有均等的执行机会,那带来的问题是:并不知道下一时刻是哪个线程被运行,也不知道它正要执行的代码是什么。所以就可能存在竞态条件。

例如爬虫工作线程从任务队列拿待抓取URL的时候,如果多个爬虫线程同时来取,那这个任务到底该给谁?那就需要用到“锁”或“同步队列”来保证下载任务不会被重复执行。

而且线程支持的多任务规模,在数百到数千的数量规模。在大规模的高频网络交互系统中,仍然有些吃力。当然,多线程最主要的问题还是竞态条件。

3.4 非阻塞方式

先来看看最原始的非阻塞如何工作的。

总体耗时约4.3秒。

第9行代码sock.setblocking(False)告诉OS,让socket上阻塞调用都改为非阻塞的方式。上述代码在执行完 sock.connect() 和 sock.recv() 后的确不再阻塞,可以继续往下执行请求准备的代码或者是执行下一次读取。

代码变得更复杂也是上述原因所致。第11行要放在try语句内,是因为socket在发送非阻塞连接请求过程中,系统底层也会抛出异常。connect()被调用之后,立即可以往下执行第15和16行的代码。

需要while循环不断尝试 send(),是因为connect()已经非阻塞,在send()之时并不知道 socket 的连接是否就绪,只有不断尝试,尝试成功为止,即发送数据成功了。recv()调用也是同理。

虽然 connect() 和 recv() 不再阻塞主程序,空出来的时间段CPU没有空闲着,但并没有利用好这空闲去做其他有意义的事情,而是在循环尝试读写 socket (不停判断非阻塞调用的状态是否就绪)。还得处理来自底层的可忽略的异常。也不能同时处理多个 socket 。

然后10次下载任务仍然按序进行。所以总体执行时间和同步阻塞相当。

3.5 非阻塞改进

3.5.1 epoll

判断非阻塞调用是否就绪如果 OS 能做,是不是应用程序就可以不用自己去等待和判断了,就可以利用这个空闲去做其他事情以提高效率。

所以OS将I/O状态的变化都封装成了事件,如可读事件、可写事件。并且提供了专门的系统模块让应用程序可以接收事件通知。这个模块就是select。让应用程序可以通过select注册文件描述符和回调函数。当文件描述符的状态发生变化时,select 就调用事先注册的回调函数。

select因其算法效率比较低,后来改进成了poll,再后来又有进一步改进,Linux内核改进成了epoll模块。

3.5.2 回调(Callback)

把I/O事件的等待和监听任务交给了 OS,那 OS 在知道I/O状态发生改变后(例如socket连接已建立成功可发送数据),它又怎么知道接下来该干嘛呢?回调。

需要我们将发送数据与读取数据封装成独立的函数,让epoll代替应用程序监听socket状态时,得告诉epoll:“如果socket状态变为可以往里写数据(连接建立成功了),请调用HTTP请求发送函数。如果socket 变为可以读数据了(客户端已收到响应),请调用响应处理函数。”

于是我们利用epoll结合回调机制重构爬虫代码:

来看看改进在哪。

首先,不断尝试send() 和 recv() 的两个循环被消灭掉了。

其次,导入了selectors模块,并创建了一个DefaultSelector 实例。Python标准库提供的selectors模块是对底层select/poll/epoll/kqueue的封装。DefaultSelector类会根据 OS 环境自动选择最佳的模块,那在 Linux 2.5.44 及更新的版本上都是epoll了。

然后,在第25行和第31行分别注册了socket可写事件(EVENT_WRITE)和可读事件(EVENT_READ)发生后应该采取的回调函数。

虽然代码结构清晰了,阻塞操作也交给OS去等待和通知了,但是,我们要抓取10个不同页面,就得创建10个Crawler实例,就有20个事件将要发生,那如何从selector里获取当前正发生的事件,并且得到对应的回调函数去执行呢?

3.5.3 事件循环(Event Loop)

为了解决上述问题,我们写一个循环,去访问selector模块,等待它告诉我们当前是哪个事件发生了,应该对应哪个回调。这个等待事件通知的循环,称之为事件循环。

selector.select()是一个阻塞调用,因为如果事件不发生,那应用程序就没事件可处理,所以就干脆阻塞在这里等待事件发生。所以,selector机制(后文以此称呼代指epoll/kqueue)是设计用来解决大量并发连接的。当系统中有大量非阻塞调用,能随时产生事件的时候,selector机制才能发挥最大的威力。

下面是如何启创建10个下载任务和启动事件循环的:

总体耗时约0.45秒。

上述代码异步执行的过程:创建Crawler 实例;

调用fetch方法,会创建socket连接和在selector上注册可写事件;

fetch内并无阻塞操作,该方法立即返回;

重复上述3个步骤,将10个不同的下载任务都加入事件循环;

启动事件循环,进入第1轮循环,阻塞在事件监听上;

当某个下载任务EVENT_WRITE被触发,回调其connected方法,第一轮事件循环结束;

进入第2轮事件循环,当某个下载任务有事件触发,执行其回调函数;此时已经不能推测是哪个事件发生,因为有可能是上次connected里的EVENT_READ先被触发,也可能是其他某个任务的EVENT_WRITE被触发;(此时,原来在一个下载任务上会阻塞的那段时间被利用起来执行另一个下载任务了)

循环往复,直至所有下载任务被处理完成

3.5.4 总结

目前为止,我们已经从同步阻塞学习到了异步非阻塞。掌握了在单线程内同时并发执行多个网络I/O阻塞型任务的黑魔法。而且与多线程相比,连线程切换都没有了,执行回调函数是函数调用开销,在线程的栈内完成,因此性能也更好,单机支持的任务规模也变成了数万到数十万个。(不过我们知道:没有免费午餐,也没有银弹。)

部分编程语言中,对异步编程的支持就止步于此(不含语言官方之外的扩展)。需要程序猿直接使用epoll去注册事件和回调、维护一个事件循环,然后大多数时间都花在设计回调函数上。

通过本节的学习,我们应该认识到,不论什么编程语言,但凡要做异步编程,上述的“事件循环+回调”这种模式是逃不掉的,尽管它可能用的不是epoll,也可能不是while循环。

为什么我们在某些异步编程中并没有看到 CallBack 模式呢?这就是我们接下来要探讨的问题。

4 Python 对异步I/O的优化之路

4.1 回调之痛,以终为始

考虑如下问题:如果回调函数执行不正常该如何?

如果回调里面还要嵌套回调怎么办?要嵌套很多层怎么办?

如果嵌套了多层,其中某个环节出错了会造成什么后果?

如果有个数据需要被每个回调都处理怎么办?

……

在实际编程中,上述系列问题不可避免。在这些问题的背后隐藏着回调编程模式的一些缺点:回调层次过多时代码可读性差

def callback_1():

# processing ...

def callback_2():

# processing.....

def callback_3():

# processing ....

def callback_4():

#processing .....

def callback_5():

# processing ......

async_function(callback_5)

async_function(callback_4)

async_function(callback_3)

async_function(callback_2)

async_function(callback_1)破坏代码结构

写同步代码时,关联的操作时自上而下运行:

do_a()

do_b()

如果 b 处理依赖于 a 处理的结果,而 a 过程是异步调用,就不知 a 何时能返回值,需要将后续的处理过程以callback的方式传递给 a ,让 a 执行完以后可以执行 b。代码变化为:

do_a(do_b())

Jesse comment:应该是do_b(do_a())吧??额。。。。

如果整个流程中全部改为异步处理,而流程比较长的话,代码逻辑就会成为这样:

do_a(do_b(do_c(do_d(do_e(do_f(......))))))

上面实际也是回调地狱式的风格,但这不是主要矛盾。主要在于,原本从上而下的代码结构,要改成从内到外的。先f,再e,再d,…,直到最外层 a 执行完成。在同步版本中,执行完a后执行b,这是线程的指令指针控制着的流程,而在回调版本中,流程就是程序猿需要注意和安排的。共享状态管理困难

回顾第3节爬虫代码,同步阻塞版的sock对象从头使用到尾,而在回调的版本中,我们必须在Crawler实例化后的对象self里保存它自己的sock对象。如果不是采用OOP的编程风格,那需要把要共享的状态接力似的传递给每一个回调。多个异步调用之间,到底要共享哪些状态,事先就得考虑清楚,精心设计。

错误处理困难

一连串的回调构成一个完整的调用链。例如上述的 a 到 f。假如 d 抛了异常怎么办?整个调用链断掉,接力传递的状态也会丢失,这种现象称为调用栈撕裂。 c 不知道该干嘛,继续异常,然后是 b 异常,接着 a 异常。好嘛,报错日志就告诉你,a 调用出错了,但实际是 d 出错。所以,为了防止栈撕裂,异常必须以数据的形式返回,而不是直接抛出异常,然后每个回调中需要检查上次调用的返回值,以防错误吞没。

如果说代码风格难看是小事,但栈撕裂和状态管理困难这两个缺点会让基于回调的异步编程很艰难。所以不同编程语言的生态都在致力于解决这个问题。才诞生了后来的Promise、Co-routine等解决方案。

to be continued...

深入理解python异步编程_深入理解Python异步编程相关推荐

  1. Python代码列主元消去法matlab编程_工业机器人用什么语言编程的?

    曾经有很多小伙伴一直问,工业机器人编程用的是什么语言啊?这次给大家总结一下机器人编程中常用的语言. 1.硬件描述语言(HDLs) 硬件描述语言一般是用来描述电气的编程方式.这些语言对于一些机器人专家来 ...

  2. python怎样编程_怎么自学python编程

    如何自学Python编程?一堆的Python教程却感觉无从下手呢?我想这应该是很多Python初学者正在纠结的问题. 今天想要分享给大家的是如何自学Python编程,学习这件事 还真不是人人都擅长的, ...

  3. 数学不好python好学吗_我数学不好、编程零基础、不以编程谋生,自学 Python 失败,为什么放不下编程,总是想突破它?...

    看到了题主的问题,我就没有把题主说的那么多一大串全都看完,我就从题主的标题来分析一下吧!我也是数学不好,我甚至不明白函数的原理,也不知道函数是什么,所以我一听到函数就会头疼.在经历上,我的计算机基础知 ...

  4. 高中信息技术python及答案_高中信息技术Python编程教学

    高中信息技术 Python 编程教学 张海杰 ; 刘洪胜 [期刊名称] <信息周刊> [年 ( 卷 ), 期] 2019(000)051 [摘要] 在众多的程序设计语言中 ,Python ...

  5. python是什么编程教程-编程python是什么_谁的Python教程最好?

    谁的Python教程最好? 建议你可以看看这里的<Python基础教程>和<Python学习手册>应该适合你的. 希望对你有用. 记得采纳呀~ Python中的9个代码小实例! ...

  6. python 判断类型_青少年之Python编程课程安排lt;第一季gt;

    第一章    开启Python之旅 1.   你将了解什么是Python 2.   在电脑上安装并简单使用Python 3.   开始通过Python与计算机进行交流(编程) 第二章    变量 1. ...

  7. 业余学习python有用吗_对于那些不做编程工作的小伙伴来说,学习Python有什么用呢?...

    很多同学会说Python那么火,铺天盖地的都是他的广告,可是我的平时工作和学习又接触不到编程之类的东西,那来学习它又有什么用呢? 有没有这个必要呢?在此,小编对于有这种疑问的同学呢想对你们说,其实即便 ...

  8. python qt5 gui快速编程_现货正版 Python Qt GUI与数据可视化编程 pyqt5教程书籍 pyqt5快速开发与实战Qt5 GUI快速编程 计算机网络程序设计人民邮电出版社...

    热销单品 查看更多 > RMB:85.00 立即购买 RMB:63.50 立即购买 RMB:73.50 立即购买 RMB:49.50 立即购买 RMB:127.80 立即购买 RMB:66.00 ...

  9. python 在线编辑_科技学堂Python在线编程工具发布,欢迎各位老师一起来测评!...

    原标题:科技学堂Python在线编程工具发布,欢迎各位老师一起来测评! 作为一家面向科技工作者和爱好者的在线教育的平台,科技学堂一直致力于为大家提供更多.更丰富的科技教育资源. 2019年,我们上线了 ...

最新文章

  1. 原创 | 《相机标定》深入理解原理与实战(一)
  2. 谁扛起张一鸣的游戏野心?
  3. 【转】数据库的锁机制
  4. swf批量转png_CAD批量打印(探索者易打软件)优势介绍
  5. springboot2.0版本后配置拦截器会导致静态资源被拦截
  6. python创建时间序列_python时间序列按频率生成日期
  7. C语言中从键盘中输入到数组,//从键盘上输入若干整数,并将其存入数组中,并统计输入数据的个...
  8. 快速排序及快速选择问题
  9. Entity Framework 延伸系列目录
  10. java基础练习(持续更新)
  11. 使用verilog实现4选1数据选择器的几种方法
  12. 中科大2021计算机应用数学期末回忆版
  13. 教学用计算机房活荷载,计算机机房承重标准及承重计算方法
  14. 关于集合set()补充
  15. apollo学习之---(19)commen-filter学习
  16. java 内存很高_Java服务器内存和CPU占用过高的原因
  17. EasyExcel的使用
  18. LSM-tree基本原理及应用
  19. 如何驯服事件驱动的微服务
  20. html中使用css实现版心定位

热门文章

  1. .以及JDK1.5ConcurrentHashMap新特性
  2. Android 判断当前联网的类型 wifi、移动数据流量
  3. /usr/local/php-5.2.14/sbin/php-fpm start Starting php_fpm –fpm-config
  4. python selenium 弹窗获取元素_python中能否使用selenium获取弹窗的文本内容?
  5. linux编程能否用于windows,使R包在Windows和Linux中都可以工作
  6. rtsp 测试地址_TranServer:简单实现浏览器播放RTSP流
  7. oracle表对比同步,Oracle表双向同步问题
  8. php旋转数组找出最小的,LeetCode 153 寻找旋转排序数组中的最小值
  9. redhat7 32位mysql_Redhat7.3安装MySQL8.0.22的详细教程(二进制安装)
  10. java中的枚举类_java中的枚举类型