“按史索骥”:Python异步编程之协程进化史

Posted OneAndZero

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了“按史索骥”:Python异步编程之协程进化史相关的知识,希望对你有一定的参考价值。

注1:我的行文习惯是先理论后实例,如果想直接看实例可直接看每节的后面

注2:为了保证代码段的格式,所有代码段通过图片的形式展示


在试图弄清楚整个Python的异步编程生态的时候,总有一种似懂非懂的感觉,其中一部分原因是由于Python原生支持异步编程有一段很长的进化史,而这其中又包含了生成器、协程的进化史,本文尝试对生成器的进化史以及生成器是如何支持协程的做一个较详细的梳理。


一、什么是协程

协程是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂定或开始执行程序。协同程序非常适合实如协作任务,异步,事件循环,迭代器,无限列表和管道等程序组件。

从技术的角度来说,“协程就是你可以暂停执行的函数”,听起来是不是很像Python的生成器?这么理解就对了。


二、Python yield生成器是如何支持协程的?

如果可以利用生成器“暂停”的部分,添加“将数据发送回生成器”的功能,那么 Python 突然就有了协程的概念。


2.1 “加强版”Python生成器

Python 2.5 提出了一些增强生成器的API和语法 [PEP342 Coroutines via Enhanced Generators](https://www.python.org/dev/peps/pep-0342/),使他们可以作为简单的协程使用。PEP342为生成器引入了send()方法,这让我们不仅可以暂停生成器,而且能够传递值到生成器暂停的地方。

协程是表达许多算法的自然方式,例如模拟,游戏,异步I/O以及其他形式的事件驱动编程或合作多任务。 Python的生成器函数差不多就是协程 ,但并不完全:因为Python的生成器允许暂停执行并产生一个值,但是当执行恢复时不提供传入的值或异常。它们也不允许在try/finally块的try部分中暂停执行,因此使得中止后的协程无法自行清理。

另外,当其他函数执行时,生成器不能产生控制,除非这些函数本身表示为生成器,而外部生成器是根据内部生成器生成的值来生成的。这使得即使是相对简单的用例,例如异步通信,也会变得复杂,因为调用任何函数都需要生成器阻塞(即无法生成控制),否则必须在每个需要的函数调用中添加大量的样板循环代码。

但是,如果能够在生成器被挂起时将值或异常传递到生成器中,那么一个简单的协程调度器将允许协同程序在不阻塞的情况下彼此调用——这对于异步应用程序来说是一个巨大的好处。这样的应用程序可以通过将控制权交给I/O调度器来编写执行非阻塞套接字I/O的协程,直到数据发送或可用为止。同时,执行I/O的代码只需执行下面的操作:

data = (yield nonblocking_read(my_socket, nbytes))

为了暂停执行,直到nonblocking_read()协作程序产生一个值。


Python2.5开始,通过在generator-iterator类型中添加一些简单的方法,并通过两个小的语法调整,Python开发人员将能够使用生成器函数来实现协程和其他形式的协同多任务处理。 这些方法和调整是:

  1. 重新定义yield为一个表达式,而不是声明。

  2. 为generator-iterator添加一个新的send()方法,该方法恢复生成器并发送一个值,发送的值成为了当前yield表达式的结果。同时,send()方法返回下一个生成器产生的值,或者如果生成器退出时没有生成另一个值则引发StopIteration异常

  3. 为generator-iterator添加一个新的throw()方法,该方法在生成器暂停的位置返回一个异常,并返回生成器产生的下一个值,如果生成器退出时没有生成另一个值则引发StopIteration异常(如果生成器不捕获传入的异常,或引发不同的异常,则该异常传播给调用者)

  4. 为generator-iterators添加一个close()方法,该方法在生成器暂停的地方引发GeneratorExit。如果生成器随后引发StopIteration(通过正常退出或由于已关闭) 或GeneratorExit (通过不捕获该异常),close()返回其调用者。如果生成器产生一个值,则会引发RuntimeError。 如果生成器引发其他异常,则将其传播给调用者。 如果生成器由于异常或正常退出而退出,则close()不执行任何操作。

  5. 添加支持以确保在generator-iterators被垃圾收集时调用close()。

  6. 允许yield在try/finally块中使用,因为垃圾收集或明确的close()调用现在允许执行finally子句。


2.1.1 发送数据到生成器

2.1.1.1 send() 方法

send()方法带有一个参数,这个参数是发送给生成器的值。调用send(None)相当于调用生成器的next()方法。除了生成器的当前yield表达式产生的值不同以外,调用send()时带上其他任何值都是一样的的。

因为generator-iterator在生成器函数的主体顶部开始执行,因此当生成器刚被创建时没有生成器表达式接收值。因此,当generator-iterator开始前,调用send()时使用非None参数是被禁止的,这种情况下如调用send(),则会引发TypeError。因此,在和协程通信之前,必须先调用next()或者send(None)推进协程执行到第一个yield表达式,也就是激活协程。

与next()方法一样,send()方法返回由generator-iterator生成的下一个值,或者如果生成器正常退出或已经退出,则会引发StopIteration异常。 如果生成器引发未捕获的异常,它会传播给send()的调用者。

举例如下:

“按史索骥”:Python异步编程之协程进化史


2.1.1.2 新语法:yield表达式

yield声明被允许在赋值语句的右侧使用,这种情况下,它被称之为yield表达式。这个yield表达式的值为None,除非调用send()方法并传入一个非None值作为参数。yield表达式必须总是加上括号,除非它出现在赋值右侧的顶级表达式上。举例如下:

“按史索骥”:Python异步编程之协程进化史


2.1.2 使协程返回值

在Python 3.3之前,协程不支持 return语法,如下是在Python 2.7.5环境中的测试:

“按史索骥”:Python异步编程之协程进化史


在Python 3.6中测试如下:

“按史索骥”:Python异步编程之协程进化史

执行send(None),导致协程退出,然后return结果。同样的,这里还是会产生StopIteration异常。但是,异常值存放在异常的value属性中,也就是说,

return表达式将值传给了调用方,赋值给StopIteration异常的一个属性。下面使用try/except语句捕获StopIteration异常,并拿到异常value,得到return的返回结果:

“按史索骥”:Python异步编程之协程进化史

虽然这样拿到了协程的return结果,但是有点绕圈子。好在Python 3.3 中引入了一种新的语法:yield from。利用该语法,我们可以解决获取协程返回值的问题。


2.2 委派子生成器: yield from

2.2.1 PEP380概述

[PEP380: Syntax for Delegating to a Subgenerator](https://www.python.org/dev/peps/pep-0380/)  提出了一种语法来将生成器的部分操作委托给另一个生成器。 这允许将包含yield的代码段分解并放置在另一个生成器中。 此外,允许子生成器return一个值,并将该值提供给委托生成器。


相关术语:

  • 委派生成器

包含yield from <expr>表达式的生成器函数称为委派生成器。

yield from <expr>表达式对expr做的第一件事就是调用iter(expr),从中获取迭代器,因此expr可以是任何可迭代对象。在迭代器运行到耗尽状态时,此期间它会直接向委托生成器的生成器的调用者(调用方)发送或接收值。

  • 迭代器/子生成器(subgenerator)

从yield from 表达式中<iterable>部分获取迭代器(PEP380中使用"迭代器"指代子生成器)。当迭代器是另一个生成器时,允许子生成器(subgenerator)执行带有值的return语句,并且该值成为yield from 表达式(委托生成器)的yield的值。

  • 调用方

调用委派生成器的客户端代码。


2.2.2 yield from 概述

  • 迭代器产出的值都直接传递给委托生成器的调用方

  • 使用send()方法发给委托生成器的值会直接传递给子生成器。如果发送的值时None,那么会调用子生成器的__next__()方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委托生成器恢复运行。任何其他异常都谁向上冒泡,传递给委托生成器。

  • 生成器退出时,生成器(或子生成器)中的return expr表达式会出发StopIteration(expr)异常抛出

  • yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。

  • 传入委托生成器的异常,除了GeneratorExit之外都传递给子生成器的throw()方法。如果调用throw()方法时抛出StopIteration异常,委托生成器恢复运行。StopIteration之外的异常会向上冒泡,传递给委托生辰其

  •  如果把GeneratorExit异常传入委托生成器,或者在委托生成器上调用close()方法,则会调用子生成器的close()方法(如果他有的话)。如果调用 close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit异常。


2.2.3 yield from 功能示例

  • 替代for循环

    如果生成器函数需要产出另一个生成器生成的值,传统的解决方法是使用嵌套的 for 循环。如下:


    “按史索骥”:Python异步编程之协程进化史

但是,我们也可以使用yield from来做,下面是用yield from来代替内层for循环

“按史索骥”:Python异步编程之协程进化史

可以看出, yield from it 完全代替了内层的 for 循环。在这个示例中使用 yield from 是对的,而且代码读起来更顺畅,不过感觉更像是语法糖。除了代替循环之外, yield from 还会创建通道,把内层生成器直接与外层生成器的客户端联系起来。把生成器当成协程使用时,这个通道特别重要,不仅能为客户端代码生成值,还能使用客户端代码提供的值。


  • 作为协程通道

依旧以上面的求平均值的函数作为例子:

“按史索骥”:Python异步编程之协程进化史


2.3 总结

  • yield 表达式会接受 调用方 send(value)过来的值, 然后可以将这个值赋给某个变量

  •  yield from 的值是子生成器终止时传给StopIteration的异常的第一个参数


三、事件驱动型框架

事件驱动型框架(如 Tornado 和 asyncio)的运作方式:在单个线程中使用一个主循环驱动协程执行并发活动。使用协程做面向事件编程时,协程会不断把控制权让步给主循环,激活并向前运行其他协程,从而执行各个并发活动。这是一种协作式多任务:协程显式自主地把控制权让步给中央调度程序。而多线程实现的是抢占式多任务。调度程序可以在任何时刻暂停线程(即使在执行一个语句的过程中),把控制权让给其他线程。

Python 3.4中引入了一个实验性质(说白了就是个临时工)的标准库asyncio,asyncio 是一个事件驱动型框架。至此,Python也获得了事件循环的特性。

asyncio我会在后面的文章进行分析,因为这个太大了,此篇文章已经够长了。这里指提一句,在asyncio中,基于生成器的协程应使用@asyncio.coroutine进行装饰


四、原生协程:async 和 await 的引进


4.1 概述

Python 3.5 中引入了 async 和 await 语法,协程成为了Python的特性之一,而不再是使用基于生成器的协程了。使用async 和 await 定义的协程在Python中称之为原生协程,更多内容参看 [PEP492: Coroutines with async and await syntax](https://www.python.org/dev/peps/pep-0492)

PEP492通过添加awaitable对象,协程函数,异步迭代`和 异步上下文管理极大的改进了Python对异步编程的支持。

PEP492假定异步任务由类似于stdlib模块asyncio.events.AbstractEventLoop的事件循环调度和协调。虽然PEP不依赖于任何特定的事件循环实现,但它仅与使用yield作为调度程序信号的协程类型相关,指示协程将等待直到事件(例如IO)完成


4.2 原生协程语法声明

协程函数使用 async def 语法声明:

“按史索骥”:Python异步编程之协程进化史

在协程内部,await 表达式可以用来暂停协程的执行,直到结果可用。任何对象都可以等待,只要这个对象定义__await__()方法实现 awaitable协议。


协程的关键属性:

  • async def 函数始终是协程,即使函数内部未包含await表达式

  • async 函数内部包含 yield 或者 yield from表达式将引发 SyntaxError

  • 在内部,引入了两个新的代码对象标志:

    • CO_COROUTINE 标记表示原生协程(async 定义的函数)

    • CO_ITERABLE_COROUTINE 用于使基于生成器的协同程序与本机协同程序兼容(由types.coroutine() 函数设置)

  • 协程调用后返回一个 coroutine 对象

  • StopIteration 异常不会从协程传播出来,而是被RuntimeError替换

  • 当原生协程被gc回收后,如果它从未等待过,则引发RuntimeWarning


4.3 await表达式

await 表达式用于获得协程的执行结果。


这个例子中,await 和 yield from 一样,暂定执行read_data 协程,知道db.fetch 等待完成并且返回结果数据.


4.4 await表达式和yield from的异同


  • 都能接收协程

  •  yield from接受一般生成器,await不能接受,而是接受awaitable对象


4.5 协程对象和生成器的差异

  • 原生协程对象没有实现 __iter__和 __next__方法。因此,原生协程不是可迭代的或者它们不能传递给iter(), list(), tuple() 等内置函数,它们也不能用于for…in 循环

  • 一般的生成器不能从原生协程生成:这样会导致TypeError。即await不能接受一个生成器,而是接受awaitable对象

  • 基于生成器的协程(对于asyncio代码必须使用@asyncio.coroutine进行修饰)可以 yield from协程对象


五、总结

本文主要讲解的是协程以及Python是如何一步一步支持协程的。中间提到了asyncio包,他是Python中支持异步编程的一个标准库,由于篇幅原因这里不能写太多内容了,asyncio相关的内容我会在后续提到。最后,我列出一个Python协程发展的顺序,以供参看:

  1. Python 2.2 引入生成器 

  2. Python 2.5 增强生成器,加入了yield表达式和send()方法,可以作为协程使用。相关PEP: [PEP342 Coroutines via Enhanced Generators](https://www.python.org/dev/peps/pep-0342/)

  3. Python 3.3 ,引入 yield from 语法,使得重构生成器与将它们串联起来都很简单。相关PEP:[PEP380: Syntax for Delegating to a Subgenerator](https://www.python.org/dev/peps/pep-0380/) 

  4. Python 3.4 中增加了一个实验性质的模块:asyncio,这是一个支持异步IO的异步框架。相关PEP:[PEP 3156 — Asynchronous IO Support Rebooted: the “asyncio” Module | Python.org](https://www.python.org/dev/peps/pep-3156/#event-loop-interface-specification)

  5. Python 3.5 中引入了async和 await 语法,Python从此开始支持原生协程。相关PEP: [PEP492: Coroutines with async and await syntax](https://www.python.org/dev/peps/pep-0492)


如有错误之处请指正,谢谢!



以上是关于“按史索骥”:Python异步编程之协程进化史的主要内容,如果未能解决你的问题,请参考以下文章

Python--Demo18--异步IO之协程

Python异步IO之协程:使用asyncio的不同方法实现协程

Python异步IO之协程:从yield from到async的使用

python并发编程之协程

Python学习:python并发编程之协程

网络编程之协程——gevent模块