鱼C论坛

 找回密码
 立即注册
查看: 3300|回复: 6

[技术交流] Python 小技巧 033:异步协程函数

[复制链接]
发表于 2019-10-7 12:48:51 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

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

Python 的异步协程函数 async / await


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

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

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

  1. def wash():
  2.     from time import sleep
  3.     def washer1():
  4.         sleep(3)    # 第一台洗衣机,需要洗 3 秒才能洗完(打个比方)
  5.         print('washer1 finished')    # 洗完的时候,洗衣机会响一下,告诉我们洗完了
  6.    
  7.     def washer2():
  8.         sleep(2)
  9.         print('washer2 finished')
  10.    
  11.     def washer3():
  12.         sleep(5)
  13.         print('washer3 finished')
  14.    
  15.     washer1()
  16.     washer2()
  17.     washer3()
复制代码


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

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

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

  1. def wash():
  2.     from time import sleep
  3.     async def washer1():
  4.         sleep(3)
  5.         print('washer1 finished')
  6.    
  7.     async def washer2():
  8.         sleep(2)
  9.         print('washer2 finished')
  10.    
  11.     async def washer3():
  12.         sleep(5)
  13.         print('washer3 finished')
  14.    
  15.     washer1()
  16.     washer2()
  17.     washer3()
复制代码


但升级成异步函数后,运行会发出警告:
  1. RuntimeWarning: coroutine 'wash.<locals>.washer1' was never awaited
  2. RuntimeWarning: coroutine 'wash.<locals>.washer2' was never awaited
  3. RuntimeWarning: coroutine 'wash.<locals>.washer3' was never awaited
复制代码

这是为什么呢?

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

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

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

  6.     async def washer2():
  7.         await sleep(2)
  8.         print('washer2 finished')

  9.     async def washer3():
  10.         await sleep(5)
  11.         print('washer3 finished')

  12.     washer1()
  13.     washer2()
  14.     washer3()
复制代码


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

#敲黑板,重点来了!

1. 第一个问题

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

  1. import asyncio
  2. await asyncio.sleep(3)
复制代码

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

  1. await an_async_function()
复制代码

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

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


2. 第二个问题

如果我们要执行异步函数,不能用这样的调用方法:
  1. washer1()
  2. washer2()
  3. washer3()
复制代码

而应该用 asyncio 库中的事件循环机制来启动(具体见下面讲解)。

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

  1. def wash():
  2.     import time
  3.     import asyncio    # 引入 asyncio 库

  4.     start = time.time()

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

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

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

  14.     loop = asyncio.get_event_loop()

  15.     tasks = [
  16.         washer1(),
  17.         washer2(),
  18.         washer3(),
  19.     ]

  20.     loop.run_until_complete(asyncio.wait(tasks))
  21.    
  22.     loop.close()

  23.     print('运行用时:', time.time() - start)
复制代码


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

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

1. 创建一个事件循环。

  1. loop = asyncio.get_event_loop()
复制代码


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

  1. tasks = [
  2.     washer1(),
  3.     washer2(),
  4.     washer3(),
  5. ]
复制代码


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

  1. loop.run_until_complete(asyncio.wait(tasks))
复制代码


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

  1. loop.run_until_complete(asyncio.wait(tasks))
  2. loop.run_until_complete(asyncio.wait(tasks))
  3. loop.run_until_complete(asyncio.wait(tasks))
  4. ...
复制代码


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

  1. loop.close()
复制代码


最终的打印效果:

  1. washer2 finished
  2. washer1 finished
  3. washer3 finished
  4. 运行用时: 5.0031044483184814    # 毕竟切换线程也要有点耗时的
复制代码





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

评分

参与人数 1荣誉 +2 鱼币 +2 贡献 +1 收起 理由
一个账号 + 2 + 2 + 1 感谢楼主无私奉献!

查看全部评分

本帖被以下淘专辑推荐:

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2019-10-7 16:58:57 | 显示全部楼层
一个账号 发表于 2019-10-7 16:58
不错哟~
但我不想评分!!!

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2019-10-31 17:29:51 | 显示全部楼层
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2020-1-19 01:02:55 | 显示全部楼层
真的讲的好基础,好基础,而且Python协程真心没用,很多讲的是原理以及概念,现在应用的机会很少。
基本io并发操作,还是多线程顶
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2021-8-22 21:18:40 | 显示全部楼层
协程函数异常处理怎么设置呢,同步请求一般是用通过状态码来判断,那在协程函数中呢??
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2021-11-2 10:00:07 | 显示全部楼层
讲的简单易懂
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2023-3-10 09:23:31 From FishC Mobile | 显示全部楼层
可以,讲的很好理解
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-4-20 20:44

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表