zltzlt 发表于 2019-10-7 12:48:51

Python 小技巧 033:异步协程函数

本帖最后由 zltzlt 于 2019-10-7 12:53 编辑

Python 的异步协程函数 async / await

看到题目,很多鱼油可能会问:异步协程函数是什么?先别着急,马上你就知道了。先举一个例子:

1. 假设我们有三台洗衣机,现在有三批衣服需要分别放到这三台洗衣机里面洗。

相信大家都会写出这样的代码:

def wash():
    from time import sleep
    def washer1():
      sleep(3)    # 第一台洗衣机,需要洗 3 秒才能洗完(打个比方)
      print('washer1 finished')    # 洗完的时候,洗衣机会响一下,告诉我们洗完了
   
    def washer2():
      sleep(2)
      print('washer2 finished')
   
    def washer3():
      sleep(5)
      print('washer3 finished')
   
    washer1()
    washer2()
    washer3()

但是,这样写虽然能正确运行,洗衣机会洗衣服,不会崩溃(报错),但效率太低,大部分时间都花在挨个地等洗衣机上了,这不是我们想要的。

2. 我们要避免无味的等待,为了提高效率,我们将使用 async(async 是 Python 3.4 新添加的关键字)。

washer1()、washer2()、washer3() 本来是普通函数,现在我们用 async 将它们升级成 “异步函数”。一个异步函数,有个更标准的称呼,我们叫它 “协程” (coroutine)。

def wash():
    from time import sleep
    async def washer1():
      sleep(3)
      print('washer1 finished')
   
    async def washer2():
      sleep(2)
      print('washer2 finished')
   
    async def washer3():
      sleep(5)
      print('washer3 finished')
   
    washer1()
    washer2()
    washer3()

但升级成异步函数后,运行会发出警告:
RuntimeWarning: coroutine 'wash.<locals>.washer1' was never awaited
RuntimeWarning: coroutine 'wash.<locals>.washer2' was never awaited
RuntimeWarning: coroutine 'wash.<locals>.washer3' was never awaited
这是为什么呢?

从正常人的理解来看,我们现在有了异步函数,但是却忘了定义应该什么时候 “离开” 一台洗衣机,去开另一个 …… 这就会导致,现在的情况是我们一边看着第一台洗衣机,一边着急地想着 “是不是该去开第二台洗衣机了呢?” 但又不敢去, 最终还是花了 10 秒的时间才把衣服洗完。

3. 现在我们吸取了上次的教训,告诉自己洗衣服的过程是 “可等待的” (awaitable)。在它开始洗衣服的时候,我们可以去弄别的机器。

def wash():
    from time import sleep
    async def washer1():
      await sleep(3)    # 注意这里加入了 await
      print('washer1 finished')

    async def washer2():
      await sleep(2)
      print('washer2 finished')

    async def washer3():
      await sleep(5)
      print('washer3 finished')

    washer1()
    washer2()
    washer3()

尝试运行一下,我们会发现还是会发出警告,警告内容和上面的一样。这是为什么呢?{:10_272:}这里我给大家解释一下。

#敲黑板,重点来了!

1. 第一个问题

await 后面必须跟一个 awaitable 类型或者具有 __await__ 方法的对象。这个 awaitable 对象,并不是我们认为 time.sleep() 是 awaitable(可等待的)就可以 await 了,常见的 awaitable 对象应该是:

import asyncio
await asyncio.sleep(3)
asyncio 库的 sleep() 机制与 time.sleep() 不同,前者是 “假性睡眠”,后者是会导致线程阻塞的 “真性睡眠”。

await an_async_function()
一个异步函数(async)也是可等待的对象。

以下是不可等待的:
await time.sleep(3)
await 'hello'    # <class 'str'> 没有定义 __await__ 方法
await 3 + 2    # <class 'int'> 没有定义 __await__ 方法
await None    # <class 'NoneType'> 没有定义 __await__ 方法
await a_sync_function()    # 普通的函数是不可等待的

2. 第二个问题

如果我们要执行异步函数,不能用这样的调用方法:
washer1()
washer2()
washer3()
而应该用 asyncio 库中的事件循环机制来启动(具体见下面讲解)。

4. 下面是最终我们想要的实现。

def wash():
    import time
    import asyncio    # 引入 asyncio 库

    start = time.time()

    async def washer1():
      await asyncio.sleep(3)    # 使用 asyncio.sleep(),它返回的是一个可等待的对象
      print('washer1 finished')

    async def washer2():
      await asyncio.sleep(2)
      print('washer2 finished')

    async def washer3():
      await asyncio.sleep(5)
      print('washer3 finished')

    loop = asyncio.get_event_loop()

    tasks = [
      washer1(),
      washer2(),
      washer3(),
    ]

    loop.run_until_complete(asyncio.wait(tasks))
   
    loop.close()

    print('运行用时:', time.time() - start)

接下来我将详细讲解代码。

事件循环机制分为以下几步骤:

1. 创建一个事件循环。

loop = asyncio.get_event_loop()

2. 将异步函数加入事件队列。

tasks = [
    washer1(),
    washer2(),
    washer3(),
]

3. 执行事件队列,直到最晚的一个事件被处理完毕后结束。

loop.run_until_complete(asyncio.wait(tasks))

PS:如果不满意想要多洗几遍,可以多写几句:

loop.run_until_complete(asyncio.wait(tasks))
loop.run_until_complete(asyncio.wait(tasks))
loop.run_until_complete(asyncio.wait(tasks))
...

4. 如果不再使用 loop,建议养成良好关闭的习惯(有点类似于文件读写结束时的 close() 操作)。

loop.close()

最终的打印效果:

washer2 finished
washer1 finished
washer3 finished
运行用时: 5.0031044483184814    # 毕竟切换线程也要有点耗时的



如果这篇文章让你学习到新知识,不要忘记点下面的 “评分” 作为奖励哦 ~ ^_^

zltzlt 发表于 2019-10-7 16:58:57

一个账号 发表于 2019-10-7 16:58
不错哟~
但我不想评分!!!

{:10_264:}

122815306 发表于 2019-10-31 17:29:51

{:10_279:}

四点好 发表于 2020-1-19 01:02:55

真的讲的好基础,好基础,而且Python协程真心没用,很多讲的是原理以及概念,现在应用的机会很少。
基本io并发操作,还是多线程顶

一坨屎吖 发表于 2021-8-22 21:18:40

协程函数异常处理怎么设置呢,同步请求一般是用通过状态码来判断,那在协程函数中呢??

zy1257 发表于 2021-11-2 10:00:07

讲的简单易懂

雾隐花川夜 发表于 2023-3-10 09:23:31

可以,讲的很好理解
页: [1]
查看完整版本: Python 小技巧 033:异步协程函数