代码环境:python3.6
上一篇文章我们讲了 python 中多线程的使用:点击阅读,现在我们讲讲 python 中的协程。
异步IO
我们知道,CPU 速度远远快于磁盘、网络等 IO。在 IO 编程中,假如一个 IO 操作阻塞了当前线程,会导致其他代码无法执行,所以我们使用多线程或者多进程来并发执行代码。
但是,系统资源是有限的,一旦线程数量过多,CPU 的时间就花在线程切换上了,真正执行代码的时间下降,导致性能严重下降。
针对这个问题,我们需要另一种解决方法:异步 IO。
异步 IO,即当代码需要执行一个耗时的 IO 操作时,它只发出 IO 指令,并不等待 IO 结果,然后就去执行其他代码了。一段时间后,当 IO 返回结果时,再通知 CPU 进行处理。
python中最初的协程
了解最初的协程有助于我们理解后面现代协程的用法。
协程这个概念并不是 python 首次提出的,而是从其他语言借鉴过来的。
我们知道,两个普通函数的调用是按顺序的,比如A函数调用B函数,B执行完毕返回结果给A,A执行完毕。
协程看上去也是函数,如果协程A调用协程B,在执行过程中,协程B可以中断,转而执行A,再在适当的时候返回B接着从中断处往下执行。
协程的这种执行特点,恰好符合我们的需求:通过协程实现异步 IO 编程。
生成器进化成协程
python 基于 generator 进行一系列功能改进后得到协程,语法上都是定义体中包含 yield 关键字。
在协程中,yield 不仅可以返回值,还能接收调用者通过.send()
方法发出的参数。yield 通常出现在表达式右边,如:data = yield something
。如果 yield 后面没有表达式,说明此时 yield 只负责接收数据,协程始终返回None
。
简单协程的基本行为
举个简单例子:
In [1]: def my_coroutine():
...: print('协程被激活')
...: while True:
# yield 后面不跟表达式,这里只接收 send() 传过来的数据
...: x = yield
...: print(f'协程接收到参数:{x}')
...:
In [2]: my_corou = my_coroutine()
# 可查看协程当前状态
In [3]: from inspect import getgeneratorstate
In [4]: getgeneratorstate(my_corou)
Out[4]: 'GEN_CREATED'
# 激活协程,此处可用 my_corou.send(None) 代替
In [5]: next(my_corou)
协程被激活
In [6]: getgeneratorstate(my_corou)
Out[6]: 'GEN_SUSPENDED'
In [7]: return_value = my_corou.send(99)
协程接收到参数:99
In [8]: print(return_value)
None
In [9]: my_corou.close()
In [10]: getgeneratorstate(my_corou)
Out[10]: 'GEN_CLOSED'
通过例子我们主要了解的是,协程需要手动激活才能真正调用,协程在不需要的时候要记得关闭。
用协程改进生产者-消费者模型
传统生产者-消费者模型中,一个线程写消息,另一个线程取消息,通过锁机制控制队列和等待,但是一不小心可能死锁。
如果改用协程,生产者生产消息后,直接通过 yield 跳转到消费者开始执行,待消费者执行完毕后,再切换回生产者继续生产,整个流程无锁且效率高:
from inspect import getgeneratorstate
def consumer():
r = '200 OK'
while True:
# yield接收生产者的数据赋值给n,并把处理结果状态r返回
n = yield r
print(f'[CONSUMER] 消费了:{n}')
def producer(c):
# 别忘了激活协程
c.send(None)
n = 0
while n < 5:
n = n + 1
print(f'[PRODUCER] 生产了:{n}')
# 一旦生产了东西,通过c.send()切换到consumer执行
# consumer处理数据后通过yield返回结果状态,这里获取返回内容
r = c.send(n)
print(f'[PRODUCER] 消费者返回的处理结果:{r}')
print(f'生产者不生产了,看看当前consumer状态:{getgeneratorstate(c)}')
c.close()
print(f'关闭consumer,看看当前consumer状态:{getgeneratorstate(c)}')
if __name__ == "__main__":
producer(consumer())
上面例子整个流程只由一个线程执行且无锁,生产者和消费者协作完成任务,这种属于协作式多任务,跟多线程这种抢占式多任务要区分开。
asyncio
在 python3.4 版本中,开始引入标准库asyncio
直接内置了对异步 IO 的支持。
asyncio
的编程模型就是一个消息循环。我们从asyncio
模块中直接获取一个EventLoop
的引用,然后把需要执行的协程扔到EventLoop
中执行,就实现了异步 IO。
先简单介绍下asyncio
涉及到的一些词语:
-
Future
:一个对象,表示异步执行的操作。通常情况下自己不应该创建Future
,而只能由并发框架如asyncio
实例化。 -
Task
:在EventLoop
中负责执行协程的任务,是Future
的子类。换句话说,Task
就是Future
,但反过来不一定。
下面是asyncio
常用API:
-
asyncio.get_event_loop()
:获取一个EventLoop
对象,用来运行协程 -
asyncio.iscoroutine(obj)
:判断一个对象是否是协程。 -
asyncio.sleep(delay)
:直接当做是一个耗时多少秒的协程即可。 -
asyncio.ensure_future(coro_or_future)
:入参是协程,则激活协程,返回一个Task
对象;如果入参是Future
,则将入参直接返回。 -
asyncio.gather(coros_or_futures)
:按入参中协程的顺序保存协程的执行结果,大部分情况下使用。 -
asyncio.wait(futures)
:对比gather
,不一定按入参顺序返回执行结果。返回包含已完成和挂起的Task
,可通过接收参数return_when
选择返回结果的时机,按实际情况使用。
我们将在下面结合新的关键字async/await
来举例说明。
async/await
为了简化使用和标识异步 IO,从 python3.5 版本开始引入新的语法糖async/await
,用async
把一个generator
标记为协程函数,然后在协程内部用await
调用另一个协程实现异步操作。
注意:
用async
标记协程函数,调用该函数时协程尚未激活,激活该函数可以用await
或者yield from
,也可以通过ensure_future()
或者AbstractEventLoop.create_task()
调度执行。
举个例子:
from asyncio import sleep as aiosleep, gather, get_event_loop
async def compute(x, y):
print("计算 %s + %s ..." % (x, y))
await aiosleep(1)
return x + y
async def print_sum(x, y):
result = await compute(x, y)
print("%s + %s = %s" % (x, y, result))
async def coro_main():
'''一般我们会写一个 coroutine 的 main 函数,专门负责管理协程'''
await gather(print_sum(1, 2), print_sum(4, 9))
def main():
aioloop = get_event_loop()
# 内部使用ensure_future()激活协程
aioloop.run_until_complete(coro_main())
aioloop.close()
if __name__ == "__main__":
main()
执行结果:
计算 1 + 2 ...
计算 4 + 9 ...
(暂停约1秒,实际输出没有这行)
1 + 2 = 3
4 + 9 = 13
观察例子运行结果,我们看到:
- 当协程开始计算
1+2
前还有一个耗时 1 秒的 IO 操作,当前线程并未等待,而是去执行其他协程计算4+9
,实现了并发执行。 - 协程结果按
gather
入参的顺序打印。
总结
- 面对 CPU 高速执行和 IO 设备的龟速严重不匹配问题,我们至少要知道两种解决方法:使用多进程和多线程并发执行代码;使用异步 IO 执行代码。
- python 协程是基于生成器改进后得到的,底部实现都是定义体中包含
yield
关键字。 - 协程属于协作式多任务,整个流程无需锁,跟多线程这种抢占式多任务要区分开。
-
asyncio
支持异步 IO,我们从asyncio
模块中直接获取一个EventLoop
的引用,然后把需要执行的协程扔到EventLoop
中执行,就实现了异步 IO。 - 定义协程函数时,我们用
async
标记协程函数,然后在协程内部用await
调用另一个协程实现异步操作。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。