博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

python 协程的诞生

Posted on 2018-06-12 11:28  bw_0927  阅读(202)  评论(0)    收藏  举报

http://python.jobbole.com/88291/

https://www.cnblogs.com/my_life/articles/9075758.html

 

Tornado、Twisted、Gevent 这类异步框架

Flask、Django等传统的非异步框架

深入学习Python3中新增的asyncio库和async/await语法,尽情享受 Python 带来的简洁优雅和高效率。

 

1.1 阻塞

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

1.2 非阻塞

  • 程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。
  • 非阻塞并不是在任何程序级别、任何情况下都可以存在的。
  • 仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。

1.3 同步

  • 不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,称这些程序单元是同步执行的。
  • 例如购物系统中更新商品库存,需要用“行锁”作为通信信号,让不同的更新请求强制排队顺序执行,那更新库存的操作是同步的。
  • 简言之,同步意味着有序

1.4 异步

  • 为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式。
  • 不相关的程序单元之间可以是异步的。
  • 例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。
  • 简言之,异步意味着无序

上文提到的“通信方式”通常是指异步和并发编程提供的同步原语,如信号量、锁、同步队列等等。

我们需知道,虽然这些通信方式是为了让多个程序在一定条件下同步执行(按序执行,但正因为是异步的存在,才需要这些通信方式。

 

1.5 并发

  • 并发描述的是程序的组织结构。指程序要被设计成多个可独立执行的子任务。
  • 以利用有限的计算机资源使多个任务可以被实时或近实时执行为目的。

1.6 并行

  • 并行描述的是程序的执行状态。指多个任务同时被执行。
  • 以利用富余计算资源(多核CPU)加速完成多个任务为目的。

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

 

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

 

1.9 异步之难(nán)

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

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

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

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

  • 程序下一步行为往往依赖上一步执行结果,如何知晓上次异步调用已完成并获取结果?
  • 回调(Callback)成了必然选择。那又需要面临“回调地狱”的折磨。
  • 同步代码改为异步代码,必然破坏代码结构。
  • 解决问题的逻辑也要转变,不再是一条路走到黑,需要精心安排异步任务。

 

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

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

小提示:Python中 time.sleep 是阻塞的,都知道使用它要谨慎,但在多线程编程中,time.sleep 并不会阻塞其他线程。

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

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

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

 

 

事件循环+回调”的基本运行原理,可以基于这种方式在单线程内实现异步编程。也确实能够大大提高程序运行效率。但是,刚才所学的只是最基本的,然而在生产项目中,要应对的复杂度会大大增加。考虑如下问题:

  • 如果回调函数执行不正常该如何?
  • 如果回调里面还要嵌套回调怎么办?要嵌套很多层怎么办?
  • 如果嵌套了多层,其中某个环节出错了会造成什么后果?
  • 如果有个数据需要被每个回调都处理怎么办?
  • ……

在实际编程中,上述系列问题不可避免。在这些问题的背后隐藏着回调编程模式的一些缺点

  • 回调层次过多时代码可读性差
  • 破坏代码结构

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

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

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

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

  • 共享状态管理困难
    回顾第3节爬虫代码,同步阻塞版的sock对象从头使用到尾,而在回调的版本中,我们必须在Crawler实例化后的对象self里保存它自己的sock对象。如果不是采用OOP的编程风格,那需要把要共享的状态(这里是socket这个对象)接力似的传递给每一个回调。多个异步调用之间,到底要共享哪些状态,事先就得考虑清楚,精心设计。
  • 错误处理困难
    一连串的回调构成一个完整的调用链。例如上述的 a 到 f。假如 d 抛了异常怎么办?整个调用链断掉,接力传递的状态也会丢失,这种现象称为调用栈撕裂

     c 不知道该干嘛,继续异常,然后是 b 异常,接着 a 异常。好嘛,报错日志就告诉你,a 调用出错了,但实际是 d 出错。

          所以,为了防止栈撕裂,异常必须以数据的形式返回,而不是直接抛出异常,然后每个回调中需要检查上次调用的返回值,以防错误吞没

 

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

事件循环+回调的基础上衍生出了基于协程的解决方案,代表作有 Tornado、Twisted、asyncio 等。

接下来我们随着 Python 生态异步编程的发展过程,深入理解Python异步编程。

 

4.2 核心问题

通过前面的学习,我们清楚地认识到异步编程最大的困难异步任务何时执行完毕?接下来要对异步调用的返回结果做什么操作?

上述问题我们已经通过事件循环回调解决了。

但是回调会让程序变得复杂。要异步,必回调,又是否有办法规避其缺点呢?那需要弄清楚其本质,为什么回调是必须的?还有使用回调时克服的那些缺点又是为了什么?

答案是程序为了知道自己已经干了什么?正在干什么?将来要干什么?换言之,程序得知道当前所处的状态,而且要将这个状态在不同的回调之间延续下去。

多个回调之间的状态管理困难,那让每个回调都能管理自己的状态怎么样?链式调用会有栈撕裂的困难,让回调之间不再链式调用怎样?

不链式调用的话,那又如何让被调用者知道已经完成了?那就让这个回调通知那个回调如何?而且一个回调,不就是一个待处理任务吗?

任务之间得相互通知,每个任务得有自己的状态(这就是协程)。那不就是很古老的编程技法:协作式多任务?然而要在单线程内做调度,啊哈,协程!

每个协程具有自己的栈帧(暂停后,知道将来从何处恢复执行),当然能知道自己处于什么状态,协程之间可以协作那自然可以通知别的协程

 

4.3 协程

  • 协程(Co-routine),即是协作式的例程。

它是非抢占式的多任务子例程的概括,可以允许有多个入口点在例程中确定的位置来控制程序的暂停与恢复执行。