python 模块 (1)asyncio 协程异步
文章目录
python 模块 (1)asyncio 协程异步
1. asyncio介绍
2. 协程基础
3. 让协程返回值以及yield from说明
4. yield from 说明
5. 异步IO(asyncio)原理
6. asyncio API基础
7. async 函数的示例
8. asyncio三种执行协程的机制:
9. “可等待”对象(Awaitables)
10. Coroutine(协程)
11. Task(任务)
12. Future(未来对象)
13. 执行asyncio程序
1. asyncio介绍
异步IO:就是发起一个IO操作(如:网络请求,文件读写等),这些操作一般是比较耗时的,不用等待它结束,可以继续做其他事情,结束时会发来通知。
协程:又称为微线程,在一个线程中执行,执行函数时可以随时中断,由程序(用户)自身控制,执行效率极高,与多线程比较,没有切换线程的开销和多线程锁机制。
python中异步IO操作是通过asyncio来实现的。
2. 协程基础
从语句上看,协程和生成器类似,都是包含了yield关键字,不同之处在于协程中yield关键词通常出现在=右边,可以产出值a(y = yield a)或不产出值时为None(y = yield)。调用方可以用send函数发送值给协程。
激活协程时在yield处暂停,等待调用方发送数据,下次继续在yield暂停。从根本上看,yield是流程控制的工具,可以实现协作式多任务,这也是后面讲解异步IO的基础。
示例1:
#!/usr/bin/python def coroutine_example(name): print('start coroutine...name:', name) x = yield name #调用next()时,产出yield右边的值后暂停;调用send()时,产出值赋给x,并往下运行 print('send值:', x) if __name__ == '__main__': coro = coroutine_example('Zarten') print('next的返回值:', next(coro)) print('send的返回值:', coro.send(6))
输出:
$ python3 ns1.py start coroutine...name: Zarten next的返回值: Zarten send值: 6 Traceback (most recent call last): File "ns1.py", line 11, in <module> print('send的返回值:', coro.send(6)) StopIteration
必须先调用next()函数预激活协程,不然send()函数无法使用。
调用next()时,产出yield右边的值后暂停不再往yield的下一行执行(一般不需要next产出值),等待send的到来,调用send()时,产出值赋给x(可对x作进一步处理),并往下运行。
协程结束时会跟生成器一样抛出StopIteration的异常给调用方,调用方可以捕获它后处理。
3. 让协程返回值以及yield from说明
获取协程的返回值
当结束协程时,会返回返回值,调用方会抛出StopIteration异常,返回值就在异常对象的value属性中。
#!/usr/bin/python def coroutine_example(name): print('start coroutine...name:', name) while True: x = yield name #调用next()时,产出yield右边的值后暂停;调用send()时,产出值赋给x,并往下运行 if x is None: return 'zhihuID: Zarten' print('send值:', x) coro = coroutine_example('Zarten') next(coro) print('send的返回值:', coro.send(6)) try: coro.send(None) except StopIteration as e: print('返回值:', e.value)
4. yield from 说明
yield from跟for循环很相似,但功能更多一些
#!/usr/bin/python def for_test(): for i in range(3): yield i print(list(for_test())) def yield_from_test(): yield from range(3) print(list(yield_from_test()))
输出:
$ python3 ns3.py [0, 1, 2] [0, 1, 2]
其实yield from内部会自动捕获StopIteration异常,并把异常对象的value属性变成yield from表达式的值。
yield from x 表达式内部首先是调用iter(x),然后再调用next(),因此x是任何的可迭代对象。yield from 的主要功能就是打开双向通道,把最外层的调用方和最内层的子生成器连接起来。
下面代码展示:调用方发送的值在yield from表达式处直接传递给子生成器,并在yield from处等待子生成器的返回。
#!/usr/bin/python def coroutine_example(name): print('start coroutine...name:', name) x = yield name #调用next()时,产出yield右边的值后暂停;调用send()时,产出值赋给x,并往下运行 print('send值:', x) return 'zhihuID: Zarten' def grouper2(): result2 = yield from coroutine_example('Zarten') #在此处暂停,等待子生成器的返回后继续往下执行 print('result2的值:', result2) return result2 def grouper(): result = yield from grouper2() #在此处暂停,等待子生成器的返回后继续往下执行 print('result的值:', result) return result def main(): g = grouper() next(g) try: g.send(10) except StopIteration as e: print('返回值:', e.value) if __name__ == '__main__': main()
$ python3 ns4.py start coroutine...name: Zarten send值: 10 result2的值: zhihuID: Zarten result的值: zhihuID: Zarten 返回值: zhihuID: Zarten
从上面也可看到yield from起到一个双向通道的作用,同时子生成器也可使用yield from
调用另一个子生成器,一直嵌套下去直到遇到yield表达式结束链式。
yield from一般用于asyncio模块做异步IO
5. 异步IO(asyncio)原理
asyncio 模块最大特点就是,只存在一个线程,跟 JavaScript 一样。
由于只有一个线程,就不可能多个任务同时运行。asyncio 是"多任务合作"模式(cooperative multitasking),允许异步任务交出执行权给其他任务,等到其他任务完成,再收回执行权继续往下执行,这跟 JavaScript 也是一样的。
由于代码的执行权在多个任务之间交换,所以看上去好像多个任务同时运行,其实底层只有一个线程,多个任务分享运行时间。表面上,这是一个不合理的设计,明明有多线程多进程的能力,为什么放着多余的 CPU 核心不用,而只用一个线程呢?但是就像前面说的,单线程简化了很多问题,使得代码逻辑变得简单,写法符合直觉。
syncio 模块在单线程上启动一个事件循环(event loop),时刻监听新进入循环的事件,加以处理,并不断重复这个过程,直到异步任务结束。事件循环的内部机制,可以参考 JavaScript 的模型,两者是一样的。
6. asyncio API基础
下面介绍 asyncio 模块最主要的几个API。注意,必须使用 Python 3.7 或更高版本,早期的语法已经变了。
第一步,import 加载 asyncio 模块。
import asyncio
第二步,函数前面加上 async 关键字,就变成了 async 函数。这种函数最大特点是执行可以暂停,交出执行权。
async def main():
第三步,在 async 函数内部的异步任务前面,加上await命令。
await asyncio.sleep(1)
上面代码中,asyncio.sleep(1)
方法可以生成一个异步任务,休眠1秒钟然后结束。
执行引擎遇到await
命令,就会在异步任务开始执行之后,暂停当前 async 函数的执行,把执行权交给其他任务。等到异步任务结束,再把执行权交回 async 函数,继续往下执行。
第四步,async.run()
方法加载 async 函数,启动事件循环。
asyncio.run(main())
上面代码中,asyncio.run()
在事件循环上监听 async
函数main的执行。等到 main 执行完了,事件循环才会终止。
7. async 函数的示例
#!/usr/bin/env python3 # async.py import asyncio async def count(): print("One") await asyncio.sleep(1) print("Two") async def main(): await asyncio.gather(count(), count(), count()) asyncio.run(main())
输出:
$ python3 async.py One One One Two Two Two
在 async 函数main的里面,asyncio.gather() 方法将多个异步任务(三个 count())包装成一个新的异步任务,必须等到内部的多个异步任务都执行结束,这个新的异步任务才会结束。
三个 count() 依次执行,打印完 One,就休眠1秒钟,把执行权交给下一个 count(),所以先连续打印出三个 One。等到1秒钟休眠结束,执行权重新交回第一个 count(),开始执行 await 命令下一行的语句,所以会接着打印出三个Two。脚本总的运行时间是1秒。
作为对比,下面是这个例子的同步版本 sync.py。
#!/usr/bin/env python3 # sync.py import time def count(): print("One") time.sleep(1) print("Two") def main(): for _ in range(3): count() main()
输出:
$ python3 sync.py One Two One Two One Two