python asyncio 异步 I/O - 协程(Coroutine)与运行

2022-08-31 09:19:51

前言

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等。

  • 作者:上海-悠悠
  • 原文链接:https://blog.csdn.net/qq_27371025/article/details/123305579
    更新时间:2022-08-31 09:19:51