鱼C论坛

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

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

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

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

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

x
本帖最后由 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()

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

#敲黑板,重点来了!

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    # 毕竟切换线程也要有点耗时的




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

评分

参与人数 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-9-24 09:17

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

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