前言
Python 在 3.5 版本中引入了关于协程的语法糖 async 和 await, 在 python3.7 版本可以通过 asyncio.run() 运行一个协程。
所以建议大家学习协程的时候使用 python3.7+ 版本,本文示例代码在 python3.8 上运行的。
协程 coroutines
协程(coroutines)通过 async/await 语法进行声明,是编写 asyncio 应用的推荐方式。
例如,以下代码段(需要 Python 3.7+)
import asyncio
import time
async def fun():
print(f'hello start: {time.time()}')
await asyncio.sleep(3)
print(f'------hello end : {time.time()} ----')
# 运行
print(fun())
当我们直接使用fun() 执行的时候,运行结果是一个协程对象coroutine object
,并且会出现警告
RuntimeWarning: coroutine 'fun' was never awaited
print(fun())
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
在函数前面加了async
,这就是一个协程了,运行的时候需使用asyncio.run()来执行(需要 Python 3.7+)
import asyncio
import time
async def fun():
print(f'hello start: {time.time()}')
await asyncio.sleep(3)
print(f'------hello end : {time.time()} ----')
# 运行
asyncio.run(fun())
运行结果
hello start: 1646009849.5220373
------hello end : 1646009852.5258074 ----
协程运行三种机制
要真正运行一个协程,asyncio 提供了三种主要机制:
- asyncio.run() 函数用来运行最高层级的入口点 "fun()" 函数 (参见上面的示例。)
- 等待一个协程。 如:await asyncio.sleep(3)
- asyncio.create_task() 函数用来并发运行作为 asyncio 任务 的多个协程。
通过前面第一个示例,知道了asyncio.run()
来运行一个协程,接着看 await 等待的使用
import asyncio
import time
async def fun_a():
print(f'hello start: {time.time()}')
await asyncio.sleep(3)
print(f'------hello end : {time.time()} ----')
async def fun_b():
print(f"world start: {time.time()}")
await asyncio.sleep(2)
print(f'------world end : {time.time()} ----')
async def main():
print('start main:')
await fun_a()
await fun_b()
print('-----------end start----------')
asyncio.run(main())
运行结果
start main:
hello start: 1646010206.405429
------hello end : 1646010209.4092102 ----
world start: 1646010209.4092102
------world end : 1646010211.4115622 ----
-----------end start----------
运行的入口是main(), 遇到await 会先去执行 fun_a(),执行完成后再去执行fun_b()。
需注意的是,await 后面不能是普通函数,必须是一个可等待对象(awaitable object),Python 协程属于 可等待 对象,因此可以在其他协程中被等待。
如果一个对象能够被用在 await表达式中,那么我们称这个对象是可等待对象(awaitable object)。很多asyncio API都被设计成了可等待的。
主要有三类可等待对象:
- 协程coroutine
- 任务Task
- 未来对象Future。
在前面这个示例中,fun_a() 和 fun_b()是按顺序执行的,这跟我们之前写的函数执行是一样的,看起来没啥差别,接着看如何并发执行2个协程任务
asyncio.create_task() 函数用来并发运行作为 asyncio 任务的多个协程
import asyncio
import time
async def fun_a():
print(f'hello start: {time.time()}')
await asyncio.sleep(3)
print(f'------hello end : {time.time()} ----')
async def fun_b():
print(f"world start: {time.time()}")
await asyncio.sleep(2)
print(f'------world end : {time.time()} ----')
async def main():
print('start main:')
task1 = asyncio.create_task(fun_a())
task2 = asyncio.create_task(fun_b())
await task1
await task2
print('-----------end start----------')
asyncio.run(main())
运行结果
start main:
hello start: 1646010554.0892649
world start: 1646010554.0892649
------world end : 1646010556.108237 ----
------hello end : 1646010557.08811 ----
-----------end start----------
从运行的结果可以看到,hello start 和 world start 的开启时间是一样的,也就是2个任务是并发执行的。
并发任务的误区
当我们知道协程可以实现并发后,于是小伙伴就想小试一下,去模拟并发下载图片,或者去并发访问网站。
先看第一个误区:
把上一个示例中的 await asyncio.sleep(3)
换成 time.sleep(3)
,假设是完成任务需花费的时间。
import asyncio
import time
async def fun_a():
print(f'hello start: {time.time()}')
time.sleep(3) # 假设是执行请求花费的时间
print(f'------hello end : {time.time()} ----')
async def fun_b():
print(f"world start: {time.time()}")
time.sleep(2) # 假设是执行请求花费的时间
print(f'------world end : {time.time()} ----')
async def main():
print('start main:')
task1 = asyncio.create_task(fun_a())
task2 = asyncio.create_task(fun_b())
await task1
await task2
print('-----------end start----------')
asyncio.run(main())
运行结果
start main:
hello start: 1646010901.340716
------hello end : 1646010904.3481765 ----
world start: 1646010904.3481765
------world end : 1646010906.3518314 ----
-----------end start----------
从运行结果看到,并没有实现并发的效果。这是因为time.sleep()它是一个同步阻塞的模块,不是异步库,达不到并发的效果。
同样道理,之前很多同学学过的 requests 库,知道 requests 库可以发请求,于是套用上面的代码,也是达不到并发效果. 因为 requests 发送请求是串行的,即阻塞的。发送完一条请求才能发送另一条请求。
如果想实现并发请求,需用到发送 http 请求的异步库,如:aiohttp,grequests等。