Python Coroutine(协程)浅析
1. 协程简介
协程,即协作式程序,又称微线程、纤程,英文名Coroutine。
思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。
协程可以在运行期间的某个点上暂停执行,并在恢复运行时从暂停的点上继续执行。
协程已经被证明是一种非常有用的程序组件,不仅被python、lua、ruby等脚本语言广泛采用,而且被新一代面向多核的编程语言如golang rust-lang等采用作为并发的基本单位。
协程可以被认为是一种用户空间线程,与传统的线程相比,有2个主要的优点:
- 与线程不同,协程是自己主动让出CPU,并交付他期望的下一个协程运行,而不是在任何时候都有可能被系统调度打断。因此协程的使用更加清晰易懂,并且多数情况下不需要锁机制。
- 与线程相比,协程的切换由程序控制,发生在用户空间而非内核空间,因此切换的代价非常小。
总结起来是一句话:协程可以认为是一种用户态线程,与系统提供的线程不同点是,它需要主动让出CPU时间,而不是由系统进行调度,即控制权在程序员手上。
2. Python协程史
- Python 2.2 中的生成器让代码执行过程可以暂停 (yield)
- Python 2.5 中可以将值返回给暂停的生成器,这使得 Python 中协程的概念成为可能 (send)
- Python 3.3 中的 yield from,使得重构生成器与将它们串联起来都很简单 (yield from)
- Python 3.4 以后通过标准库 asyncio 获得了事件循环的特性 (asyncio)
- Python 3.5 使用async/await语法引入对协程的显式支持 (async/await)
- Python 3.6 增强asyncio,支持异步生成器、异步解析式
3. yield关键字
为了理解什么是 yield, 你必须理解什么是生成器(generator)。
关于生成器我的理解是是:生成器保存的是算法,需要时再计算(惰性计算)
创建生成器有两种方式:
第一种方法:把一个列表生成式的[]改成(),就创建了一个generator:
第二种方式:在函数中使用yield关键字,函数就变成了一个generator。
函数里有了yield后,执行到yield就会停住,当需要再往下算时才会再往下算。所以生成器函数即使是有无限循环也没关系,它需要算到多少就会算多少,不需要就不往下算。
例如你想要自己实现一个 range() 函数,你可以用立即计算的方式创建一个整数列表:
然而这里存在的问题是,如果你想创建从0到1,000,000这样一个很大的序列,你不得不创建能容纳1,000,000个整数的列表。
但是当加入了生成器之后,你可以不用创建完整的序列,你只需要能够每次保存一个整数的内存即可。
|
|
让函数遇到 yield 表达式时暂停执行 – 虽然在 Python 2.5 以前它只是一条语句 – 并且能够在后面重新执行,这对于减少内存使用、生成无限序列非常有用。
你有可能已经发现,生成器完全就是关于迭代器的。有一种更好的方式生成迭代器当然很好(尤其是当你可以给一个生成器对象添加 iter() 方法时),
但是人们知道,如果可以利用生成器“暂停”的部分,添加“将东西发送回生成器”的功能,那么 Python 突然就有了协程的概念(当然这里的协程仅限于 Python 中的概念;Python 中真实的协程在后面才会讨论)。
将东西发送回暂停了的生成器这一特性通过 PEP 342添加到了 Python 2.5。
与其它特性一起,PEP 342 为生成器引入了 send() 方法。这让我们不仅可以暂停生成器,而且能够传递值到生成器暂停的地方。
还是以我们的 range() 为例,你可以让序列向前或向后跳过几个值:
|
|
其实next()和send()在一定意义上作用是相似的,区别是send()可以传递yield表达式的值进去,而next()不能传递特定的值,只能传递None进去。
因此,我们可以看做next(g) == g.send(None)
需要注意的是,第一次调用时,请使用next()语句或是send(None),不能使用send发送一个非None的值,否则会出错,因为没有yield语句来接收这个值。
4. yield from
在PEP 380 为 Python 3.3 添加了 yield from之前,生成器都没有变动。
严格来说,这一特性让你能够从迭代器(生成器刚好也是迭代器)中返回任何值,从而可以干净利索的方式重构生成器。
|
|
|
|
yield from 通过让重构变得简单,也让你能够将生成器串联起来,使返回值可以在调用栈中上下浮动,而不需对编码进行过多改动。
|
|
5. asyncio
asyncio是一个基于事件循环的异步I/O库,Python3.4将其引入标准库,Python3.3可通过pip安装
asyncio包括的内容很多很复杂,这里只会做基本的两点:协同程序和事件循环。
协程的基本概念前面已经讲过,这里先来说一下事件循环
通俗来说,事件循环 “是一种等待程序分配事件或消息的编程架构”,其提供一种循环机制,让你可以“在A发生时,执行B”。基本上来说事件循环就是监听当有什么发生时,同时事件循环也关心这件事并执行相应的代码,本质上是以队列的方式来重新分配时间片。
在asyncio中事件循环扮演的是个调度器的角色,被用来安排协同程序的执行。
PEP 342中通过asyncio.coroutine装饰的函数为协程,这里的协程是和asyncio及其事件循环一起使用的。
这赋予了 Python 第一个对于协程的明确定义,也就是基于生成器的协程
这意味着突然之间所有实现了协程接口的生成器,即便它们并不是要以协程方式应用,都符合这一定义。为了修正这一点,asyncio 要求所有要用作协程的生成器必须由asyncio.coroutine修饰。
使用以下语法声明生成器协程:
|
|
yield from在asyncio模块中得以发扬光大。通过yield from,我们可以用asyncio.sleep将协程控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。
先看示例代码:
|
|
在解释上面例子之前,需要先简单了解一下asyncio.Future
Future可以理解为延迟结果的抽象,在其他语言中也称作Promise.
你可以对任何asyncio.Future对象使用 yield from,从而将其传递给事件循环,暂停协程的执行来等待某些事情的发生( future 对象并不重要,只是asyncio细节的实现)。
一旦 future 对象获取了事件循环,它会一直在那里监听,直到完成它需要做的一切。
当 future 完成自己的任务之后,事件循环会察觉到,暂停并等待在那里的协程会通过send()方法获取future对象的返回值并开始继续执行。
以上面的代码为例, 事件循环启动每一个 countdown() 协程,一直执行到遇见其中一个协程的 yield from 和 asyncio.sleep() 。这样会返回一个 asyncio.Future对象并将其传递给事件循环,同时暂停这一协程的执行。事件循环会监控这一future对象,直到倒计时1秒钟之后(同时也会检查其它正在监控的对象,比如像其它协程)。1秒钟的时间一到,事件循环会选择刚刚传递了future对象并暂停了的 countdown() 协程,将future对象的结果返回给协程,然后协程可以继续执行。这一过程会一直持续到所有的 countdown() 协程执行完毕,事件循环也被清空。稍后我会给你展示一个完整的例子,用来说明协程/事件循环之类的这些东西究竟是如何运作的,但是首先我想要解释一下async和await。
关于asyncio这里只做了简单的介绍,它其实包括以下内容,大家可以去查看官方文档:
- 事件循环
- 任务和协程
- 传输和协议
- 基于协程的流
- 子进程
- 同步原语
- 队列
6. async与await
PEP 492引入async/await语法,中明确了协程类型(原生协程),用于区别于基于生成器的协程
在以前,我们可以用生成器实现协程(PEP 342),后来又对其进行了改进,引入了yield from语法(PEP 380)。但仍有一些缺点:
- 协程和普通生成器使用相同的语法,所以很容易把它们搞混,初学者更是如此。
- 一个函数是否是一个协程,取决于它里面是否出现了yield或yield from语句。这并不明显,容易在重构函数的时候搞乱,导致出错。
- 异步调用被yield语法限制了,我们不能获得、使用更多的语法特性,比如with和for。
这个PEP把协程从生成器独立出来,成为Python的一个原生事物。这会消除协程和生成器之间的混淆,方便编写不依赖特定库的协程代码。
使用以下语法声明原生协程:
|
|
原生协程语法的关键点:
- async def函数必定是协程,即使里面不含有await语句。
- 如果在async函数里面使用yield或yield from语句,会引发SyntaxError异常。
- 协程在调用时会返回一个coroutine对象
- 协程不再抛出StopIteration异常,而是替代为RuntimeError
- 当协程进行垃圾回收时,一个从未被await的协程会抛出RuntimeWarning异常
await表达式
下面新的await表达式用于获取协程执行结果:
await与yield from相似,挂起read_data协程的执行直到db.fetch这个awaitable对象完成并返回结果数据。
原生协程与生成器协程的区别与联系
- 原生协程对象不实现__iter__和__next__方法。因此,他们不能够通过iter(),list(),tuple()和其他一些内置函数进行迭代。他们也不能用于for…in循环。在原生协程中尝试使用__iter__或者__next会触发TypeError异常。
- 未被装饰的生成器不能够yield from一个原生协程:这样会引发TypeError。
- 基于生成器的协程(asyncio代码必须使用@asyncio.coroutine)可以yield from一个原生协程。
- 对原生协程对象和原生协程函数调用inspect.isgenerator()和inspect.isgeneratorfunction()会返回False。
- 协程内部基于生成器,原生协程与生成器协程共享实现过程。类似于生成器对象,原生协程包含throw(),send()和close()方法。
7. 异步生成器与异步解析式
PEP 492 引入支持原生协程和async /await的语法到Python 3.5。 在Python 3.5实现里的一个值得注意的局限性就在于它不可能使用await和yield在同一个函数体中。
而在Python 3.6中,这个限制已解除,这使得定义异步生成器成为可能:
PEP 530 添加了对async for在list、set、dict解析式以及generator表达式中的使用支持:
result = [i async for i in aiter() if i % 2]
此外,所有解析式都支持“await”表达式:
result = [await fun() for fun in funcs if await condition()]
8. gevent使用
gevent是一个基于协同的Python网络库,它使用greenlet在libev事件循环之上提供高级同步API。
主要特性:
- 基于libev的快速事件循环
- 基于greenlet的轻量级执行单元
- 重用python标准api(event,queue)
- 协同的socket和ssl模块
- 使用标准库和第三方模块写标准阻塞socket(gevent.monkey)
- 通过线程池或c-ares执行的DNS查询。
- 内置TCP/UDP/HTTP服务器
- 支持子进程(gevent.subprocess)
- 支持线程池
下面简单介绍gevent的使用
gevent.spawn(function, args, **kwargs)
创建一个新的Greenlet对象并安排它运行function(args,**kwargs)
注意:这时function还没有启动,它的运行依赖于gevent的事件循环,只有启动事件循环,它才会被调度
gevent.sleep(seconds=0)
将当前的greenlet睡眠seconds秒
使用gevent.sleep相当于切换上下文,让出执行权
gevent.joinall
等待多个greenlet执行结束
有时需要知道greenlet运行的状态,在greenlet中有一些标志, 让你可以监视它的线程内部状态:
- started – Boolean, 指示此Greenlet是否已经启动
- ready() – Boolean, 指示此Greenlet是否已经停止
- successful() – Boolean, 指示此Greenlet是否已经停止而且没抛异常
- value – 任意值, 此Greenlet代码返回的值
- exception – 异常, 此Greenlet内抛出的未捕获异常
更多gevent api介绍
参考文档
- Python官方文档
- PEP 255 – Simple Generators
- PEP 342 – Coroutines via Enhanced Generators
- PEP 380 – Syntax for Delegating to a Subgenerator
- PEP 3156 – Asynchronous IO Support Rebooted: the “asyncio” Module
- PEP 492 – Coroutines with async and await syntax
- 廖雪峰Python教程
- PEP 525
- PEP 530
- Python 3.5协程原理
- gevent官方文档
- gevent教程