Python 协程的基本概念
在学习 Python 基础的过程中,遇到了比较难理解的地方,那就是协程。刚开始看了廖雪峰老师的博客,没怎么看懂,后面自己多方位 google 了一下,再回来看,终于看出了点眉目,在此总结下。
什么是 yield 和 yield from
yield
在学习协程之前,要先搞懂几个基本语法,那就是 yield 和 yield from,这也是陆续困扰我几天的问题,等这两个概念弄懂以后,后面的事情就比较简单了。
- yield 是一个关键字,当一个方法中带有 yield 时,它就不是一个普通的方法了,而是变成了一个所谓的“生成器”。
- 生成器不会一下子把所有值都返回给你,可以使用 next() 方法来调用,来不断取值。
- 当生成器中执行到 yield 的时候,会从 yield 处返回结果,并保留上下文,等下一次 next() 的时候,会从上次的 yield 处继续执行。
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
f = fib(10)
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
print(next(f))
上面是一个经典的斐波那契数列的生成函数,每次调用next都会从 yield 处返回结果。
输出:
1
1
2
3
5
Traceback (most recent call last):
File "test.py", line 116, in <module>
print(next(f))
StopIteration: done
遇到 return 会抛出异常,并将 return 的值包含在异常中抛出来。
一般我们不会一直调用 next() , 而是使用 for 循环:
for b in fib(5):
print(b)
输出:
1
1
2
3
5
此处没有抛出异常,原因还待解释。。
使用 yield 一个一个地返回结果有什么作用?那是因为这样可以边循环边计算,边返回结果,不用创建完整的 list ,省去大量的内存空间。
接下来的关键点,可以通过 yield 传递参数!这个地方我也是弄了好久才弄明白的。。看下面的生产者和消费者的例子:
def customer():
r = '404 empty'
while True:
print('star consume ..')
n = yield r # 2
print('consuming {} ..'.format(n))
r = '200 ok'
def producer(c):
r = c.send(None) # 1
print(r) # 3
n = 0
while n < 10:
n += 1
print('producing {} ..'.format(n))
r = c.send(n) # 4
print('consumer {} return'.format(r))
c.close()
if __name__ == '__main__':
producer(customer())
输出:
star consume ..
404 empty
producing 1 ..
consuming 1 ..
star consume ..
consumer 200 ok return
producing 2 ..
consuming 2 ..
star consume ..
consumer 200 ok return
producing 3 ..
consuming 3 ..
star consume ..
consumer 200 ok return
producing 4 ..
consuming 4 ..
star consume ..
consumer 200 ok return
producing 5 ..
consuming 5 ..
star consume ..
consumer 200 ok return
先说明一下,send 方法可以给 yield 发送参数。
程序刚开始执行到“#1”处,这里必须先调用 send(None) 一下。此处可是有讲究的,学名叫“预激”,作用是先启动一下生成器,让它先卡在 yield ,所以此时程序在“#2”处中断了,并返回 r ,随后“#3”处打印出 “404 empty” 。
接下来程序来到“#4”处,又调用了 send 方法,此时 send 参数为1,所以“#2”处被重新激活,将 n 赋值为 1,然后继续向下执行。
接着又循环来到 “#2” 处,yield 将 r 返回,此处中断,来到“#4”处继续执行。如此不断循环,直到满足条件退出循环。
像这样,producer生产完,告诉customer消费,消费完再通知producer生产,这些事情都发生在同一个线程里面,因此没有多线程的锁和资源争夺的问题。至此,协程的初步面貌就已经浮出水面。
小 tip:
调用 send(None) ,就相当于调用 next()。
yield from
Python3.3 版本的 PEP 380 中添加了 yield from 语法,PEP380 的标题是 “syntax for delegating to subgenerator”(把指责委托给子生成器的句法)。由此我们可以知道,yield from 是可以实现嵌套生成器的使用。
yield from x 表达式对x对象做的第一件事是,调用 iter(x),获取迭代器。所以要求x是可迭代对象。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,使两者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程添加异常处理的代码。
这句话理解起来很麻烦,参考以下代码:
def A():
yield from B()
yield from C()
def B():
yield '001'
yield '002'
yield '003'
def C():
yield '004'
yield '005'
if __name__ == '__main__':
for s in A():
print(s)
输出:
001
002
003
004
005
由此可见,生成器 A 通过 yield from 将任务下发给了生成器 B 和生成器 C 来执行了。yield from 可以很方便地拆分生成器,变为几个小生成器,方便代码管理。
什么是协程
根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。
换句话说就是你可以中断函数执行,转而执行别的函数。听起来就像是你正在烧水,在此期间你可以做下一个事情,而不用等水烧开。
以前我们都是用多线程和锁来做这个事情,但是时常会担心线程安全和死锁问题,多线程切换还会产生额外的开销。现在使用协程,在一个线程里面就能完成这些任务,不用担心线程问题,没有使用任何锁,大大提高了执行效率。
上面生产者和消费者的例子,使用了 yield 简单的实现了一个协程代码。
asyncio
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio的编程模型就是一个消息循环。我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
asyncio库使我们方便地实现协程,我们可以把多个协程方法扔到asyncio的消息循环中,asyncio就自动地帮我们调用并协调这些协程方法。
@asyncio.coroutine 装饰器,可以帮助我们把一个生成器装饰为协程方法。
import threading
import asyncio
@asyncio.coroutine
def hello(i):
print('Hello world! {} {}'.format(i, threading.currentThread()))
yield from asyncio.sleep(3)
print('Hello again! {} {}'.format(i, threading.currentThread()))
loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
输出
Hello world! 2 <_MainThread(MainThread, started 140736287097792)>
Hello world! 1 <_MainThread(MainThread, started 140736287097792)>
// 中间停3秒
Hello again! 2 <_MainThread(MainThread, started 140736287097792)>
Hello again! 1 <_MainThread(MainThread, started 140736287097792)>
可以看到在第一个任务执行时遇到 yield from ,这时候程序不会等着,而是马上开始下一个任务。当然先开始哪个任务是随机的。看打印出来的线程信息显示,两个任务是在同一个线程执行。
协程帮助我们在一个线程里面异步执行多个任务,里面的任务是并发进行的。
async/await
为了简化并更好地标识异步 IO,从Python 3.5 开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。
使用也非常简单,只要把原来的 @asyncio.coroutine 替换为 async ,把 yield from 替换为 await 。
修改上一节的代码:
import threading
import asyncio
async def hello(i):
print('Hello world! {} {}'.format(i, threading.currentThread()))
await asyncio.sleep(3)
print('Hello again! {} {}'.format(i, threading.currentThread()))
loop = asyncio.get_event_loop()
tasks = [hello(1), hello(2)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
协程,有什么用?
asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。
aiohttp 应运而生,它是由 asyncio 实现的 HTTP 框架,帮助我们快速地搭建一个异步的 web 应用。
使用它要先安装:
pip install aiohttp
利用它我们用一小段代码搭建一个小应用:
访问"http://127.0.0.1:8000/"根目录,首页返回
b'<h1>Index</h1>'
访问"http://127.0.0.1:8000/hello/world",根据URL参数返回文本hello, world!。
import asyncio
from aiohttp import web
async def index(request):
await asyncio.sleep(1)
return web.Response(body=b'<h1>Index</h1>', content_type='text/html')
async def hello(request):
await asyncio.sleep(1)
text = '<h1>hello, {}!</h1>'.format(request.match_info['name'])
return web.Response(body=text.encode('utf-8'), content_type='text/html')
async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv
loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()
总结
在这里,我们初步了解了下协程的基本概念。使用 yield 和 yield from ,并且利用 asyncio 库,可以组合成一个由协程组成的异步程序。async/await 帮助我们简化协程代码。利用协程可以写出很强大的 web 应用,进一步的深入我们后面再细细探究。