协程是一种比线程更轻量级的编程方式,
Python3.5 加入了关键字 async 和 await ,将生成器和协程彻底分开。
这个 PEP 把协程从生成器独立出来,成为 Python 的一个原生事物。这会消除协程和生成器之间的混淆,方便编写不依赖特定库的协程代码。
使用原生协程和相应的新语法,我们可以在异步编程时使用协程上下文管理器和迭代器。如下文所示,新的 async with 语句可以在进入、离开运行上下文时进行异步调用,而 async for 语句可以在迭代时进行异步调用。
I/O 多路复用是一个线程里会监视多个文件描述符,在其中一个或者多个描述符准备好之后,内核会通知用户进程,用户进程来处理数据。
协程是一种比线程更轻量级的并发执行方式,它允许在函数执行过程中暂停和恢复执行,以便更好地管理程序的执行流程。协程主要用于异步编程,可以在单个线程内实现并发执行,从而提高程序的效率。
在python中,协程是由生成器进化而来,而生成器又是由迭代器进化而来。我们先从迭代器开始讲解。迭代器是python中一种特殊的可迭代对象,可以用for循环对迭代器进行遍历,与其它可迭代对象如列表不一样的是,迭代器只能被遍历一次。这样的好处是每个元素都是迭代器一步步算出来的,而不是像列表这样一开始就在内存中。所有迭代器都实现了__iter__和__next__这两个特殊的方法。
下面是用python实现的一个求斐波那契数列的迭代器:
class Test:
def __init__(self, n):
self.a = 0
self.b = 1
self.n = n
def __iter__(self):
return self
def __next__(self):
self.a, self.b = self.b, self.a + self.b
if self.a > self.n:
raise StopIteration('超过了 n')
return self.a
创建迭代器类时,__iter__ 和 __next__ 方法均只能有 self 一个参数,前者的返回值为实例本身,后者的返回值为迭代器取出的元素。从这个程序中可以更清楚地看出迭代器中的元素是计算出来的,而不是像列表那样存在内存中。
生成器首先它是一个迭代器,和迭代器一样,生成器只能被遍历迭代一次,因为每次迭代的元素不是像列表元素一样,已经在内存中,而是每迭代一次生成一个元素。迭代器的优点生成器都有。生成器有两种创建方式,一种是用生成器表达式,一种是使用 Python 关键字 yield
编写的函数叫做生成器函数,函数的返回值就是生成器。
# 用生成器表达式创建生成器
g = (x**x for x in range(1, 4))
# 用yield表达式创建生成器
def fib(n):
current = 0
a = b = 1
while current < n:
yield a
a, b = b, a + b
current += 1
函数体内部有 yield 关键字的都是生成器函数,fib 是生成器函数。yield 关键字只能出现在函数中,生成器函数的执行结果是生成器,注意这里所讲的 “执行结果” 不是函数的 return 值。生成器终止时必定抛出 StopIteration 异常,for 循环可以捕获此异常,异常的 value 属性值为生成器函数的 return 值。生成器还可以使用 next 方法迭代。生成器会在 yield 语句处暂停,这是至关重要的,未来协程中的 IO 阻塞就出现在这里。
了解了迭代器和生成器,我们再来看生成器是如何进化为协程的。首先看一个示例, Python 内置模块 itertools 中有很多实用的方法,其中之一是 chain 方法,它可以接受任意数量的可迭代对象作为参数,返回一个包含所有参数中的元素的迭代器,下面我们分别用yield 关键字和yield from关键字来实现 chain 方法。
# 用yield关键字实现chain方法
def chain(*args):
for iter_obj in args:
for i in iter_obj:
yield i
c = chain({'one', 'two'}, list('ace'))
for i in c:
print(i)
# 用yield from 关键字实现chain方法
def chain(*args):
for iter_obj in args:
yield from iter_obj
c = chain({'one', 'two'}, list('ace'))
for i in c:
print(i)
可以看到 yield from 语句可以替代 for 循环,避免了嵌套循环。同 yield 一样,yield from 语句也只能出现在函数体内部,有 yield from 语句的函数叫做协程函数或生成器函数。yield from 后面接收一个可迭代对象,例如上面代码中的 iter_obj 变量,在协程中,可迭代对象往往是协程对象,这样就形成了嵌套协程。
转移控制权是 yield from 语法的核心功能,也是从生成器进化到协程的最重要一步。下面举例说明转移控制权的功能。
# File Name: transfer_control.py
import time
from faker import Faker
from functools import wraps
# 预激协程装饰器
def coroutine(func):
@wraps(func)
def wrapper(*args, **kw):
g = func(*args, **kw)
next(g)
return g
return wrapper
# 子生成器函数,这个生成器是真正做事的生成器
def sub_coro():
l = [] # 创建空列表
while True: # 无限循环
value = yield # 调用方使用 send 方法发生数据并赋值给 value 变量
if value == 'CLOSE': # 如果调用方发生的数据是 CLOSE ,终止循环
break
l.append(value) # 向列表添加数据
return sorted(l) # 返回排序后的列表
# 使用预激协程装饰器
# 创建带有 yield from 语句的父生成器函数
@coroutine
def dele_coro():
# while True 可以多次循环,每次循环会创建一个新的子生成器 sub_coro()
# 这里 while 只循环一次,创建两次 sub_coro 生成器
# 这是由调用方,也就是 main 函数决定的
# 这里之所以使用 while 循环,是因为避免父生成器终止并触发 StopIteration 异常
while True:
# yield from 会自动预激子生成器 sub_coro()
# 所以 sub_coro 在定义时不可以使用预激协程装饰器
# yield from 将捕获子生成器终止时触发的 StopIteration 异常
# 并将异常的 value 属性值赋值给等号前面的变量 l
# 也就是 l 变量的值等于 sub_coro 函数的 return 值
# yield from 还实现了一个重要功能
# 就是父生成器的 send 方法将发送值给子生成器
# 并赋值给子生成器中 yield 语句等号前面的变量 value
l = yield from sub_coro()
print('排序后的列表:', l)
print('------------------')
# 调用父生成器的函数,也叫调用方
def main():
# 生成随机国家代号的方法
fake = Faker().country_code
# 嵌套列表,每个子列表中有三个随机国家代号(字符串)
nest_country_list = [[fake() for i in range(3)] for j in range(3)]
for country_list in nest_country_list:
print('国家代号列表:', country_list)
c = dele_coro() # 创建父生成器
for country in country_list:
c.send(country) # 父生成器的 send 方法将国家代号发送给子生成器
# CLOSE 将终止子生成器中的 while 循环
# 子生成器的 return 值赋值给父生成器 yield from 语句中等号前面的变量 l
c.send('CLOSE')
if __name__ == '__main__':
main()
在 Python 3.4 中,asyncio 模块出现,此时创建协程函数须使用 asyncio.coroutine 装饰器标记。此前的包含 yield from 语句的函数既可以称作生成器函数也可以称作协程函数,为了突出协程的重要性,现在使用 asyncio.coroutine 装饰器的函数就是真正的协程函数了。在 Python 3.5 中新增了 async / await 关键字用来定义协程函数。这两个关键字是一个组合,其作用等同于 asyncio.coroutine 装饰器和 yield from 语句。此后协程与生成器就彻底泾渭分明了。
在 asyncio 模块中出现了一些新的概念,我们来认识它们:
coroutine 协程
协程对象,使用 asyncio.coroutine 装饰器装饰的函数被称作协程函数,它的调用不会立即执行函数,而是返回一个协程对象,即协程函数的运行结果为协程对象,注意这里说的 “运行结果” 不是 return 值。协程对象需要包装成任务注入到事件循环,由事件循环调用。
task 任务
将协程对象作为参数创建任务,任务是对协程对象的进一步封装,其中包含任务的各种状态。
event_loop 事件循环
如果将多线程比喻为工厂里的多个车间,那么协程就是一个车间内的多台机器。在线程级程序中,一台机器开始工作,车间内的其它机器不能同时工作,需要等上一台机器停止,但其它车间内的机器可以同时启动,这样就可以显著提高工作效率。在协程程序中,一个车间内的不同机器可以同时运转,启动机器、暂停运转、延时启动、停止机器等操作都可以人为设置。
事件循环能够控制任务运行流程,也就是任务的调用方。
def four():
start = time.time()
async def corowork(name, t):
print('[corowork] Start coroutine', name)
await asyncio.sleep(t) # 1
print('[corowork] Stop coroutine', name)
return 'Coroutine {} OK'.format(name) # 2
loop = asyncio.get_event_loop()
coroutine1 = corowork('ONE', 3) # 3
coroutine2 = corowork('TWO', 1) # 3
task1 = loop.create_task(coroutine1) # 4
task2 = loop.create_task(coroutine2) # 4
gather = asyncio.gather(task1, task2) # 5
loop.run_until_complete(gather) # 6
print('[task1] ', task1.result()) # 7
print('[task2] ', task2.result()) # 7
end = time.time()
print('运行耗时:{:.4f}'.format(end - start))
代码说明:
1、await 关键字等同于 Python 3.4 中的 yield from 语句,后面接协程对象。asyncio.sleep 方法的返回值为协程对象,这一步为阻塞运行。asyncio.sleep 与 time.sleep 是不同的,前者阻塞当前协程,即 corowork 函数的运行,而 time.sleep 会阻塞整个线程,所以这里必须用前者,阻塞当前协程,CPU 可以在线程内的其它协程中执行
2、协程函数的 return 值可以在协程运行结束后保存到对应的 task 对象的 result 方法中
3、创建两个协程对象,在协程内部分别阻塞 3 秒和 1 秒
4、创建两个任务对象
5、将任务对象作为参数,asyncio.gather 方法创建任务收集器。注意,asyncio.gather 方法中参数的顺序决定了协程的启动顺序
6、将任务收集器作为参数传入事件循环的 run_until_complete 方法,阻塞运行,直到全部任务完成
7、任务结束后,事件循环停止,打印任务的 result 方法返回值,即协程函数的 return 值
到这一步,大家应该可以看得出,上面的代码已经是异步编程的结构了,在事件循环内部,两个协程是交替运行完成的。