• 深入理解协程(二):yield from实现异步协程


    原创不易,转载请联系作者

    深入理解协程分为三部分进行讲解:

    • 协程的引入
    • yield from实现异步协程
    • async/await实现异步协程

    本篇为深入理解协程系列文章的第二篇。

    yield from

    yield from是Python3.3(PEP 380)引入的新语法。主要用于解决在生成器中不方便使用生成器的问题。主要有两个功能。

    第一个功能:让嵌套生成器不必再通过循环迭代yield,而可以直接使用yield from

    看一段代码:

    titles = ['Python', 'Java', 'C++']
    def func1(titles):
        yield titles
    
    def func2(titles):
        yield from titles
    
    for title in func1(titles):
        print(title)
    
    for title in func2(titles):
        print(title)
        
    # 输出结果
    ['Python', 'Java', 'C++']
    Python
    Java
    C++
    

    yield返回的完整的titles列表,而yield from返回的是列表中的具体元素。yield from可以看作是for title in titles: yield title的缩写。这样就可以用yield from减少了一次循环。

    第二个功能:打开双向通道,把最外层给调用方与最内层的子生成器链接起来,二者可以直接通信。

    第二个功能听起来就让人头大。我们再举一个例子进行说明:

    【举个例子】:通过生成器实现整数相加,通过send()函数想生成器中传入要相加的数字,最后传入None结束相加。total保存结果。

    def generator_1():		# 子生成器
        total = 0
        while True:
            x = yield		# 解释4
            print(f'+ {x}')
            if not x:
                break
            total += x
        return total		# 解释5
    
    def generator_2():		# 委托生成器
        while True:
            total = yield from generator_1()	# 解释3
            print(f'total: {total}')
    
    if __name__ == '__main__':		# 调用方
        g2 = generator_2()		# 解释1
        g2.send(None)			# 解释2
        g2.send(2)				# 解释6
        g2.send(3)				
        g2.send(None)			# 解释7
    
    # 输出结果
    + 2
    + 3
    + None
    total: 5
    

    说明

    解释1g2是调用generator_2()得到的生成器对象,作为协程使用。

    解释2:预激活协程g2

    解释3generator_2接收的值都会经过yield from处理,通过管道传入generator_1实例。generator_2会在yield from处暂停,等待generator_1实例传回的值赋值给total

    解释4:调用方传入的值都会传到这里。

    解释5:此处返回的total正是generator_2()中解释3处等待返回的值。

    解释6:传入2进行计算。

    解释7:在计算的结尾传入None,跳出generator_1()的循环,结束计算。

    说到这里,相信看过《深入理解协程(一):协程的引入》的朋友应该就容易理解上面这段代码的运行流程了。

    借助上面例子,说明一下随yield from一起引入的3个概念:

    • 子生成器

      yield from获取任务并完成具体实现的生成器。

    • 委派生成器

      包含有 yield from表达式的生成器函数。负责给子生成器委派任务。

    • 调用方

      指调用委派生成器的客户端代码。

    在每次调用send(value)时,value不是传递给委派生成器,而是借助yield fromvalue传递给了子生成器的yield

    结合asyncio实现异步协程

    asyncio是Python 3.4 试验性引入的异步I/O框架(PEP 3156),提供了基于协程做异步I/O编写单线程并发代码的基础设施。其核心组件有事件循环(Event Loop)、协程(Coroutine)、任务(Task)、未来对象(Future)以及其他一些扩充和辅助性质的模块。

    在引入asyncio的时候,还提供了一个装饰器@asyncio.coroutine用于装饰使用了yield from的函数,以标记其为协程。

    在实现异步协程之前,我们先看一个同步的案例:

    import time
    def taskIO_1():
        print('开始运行IO任务1...')
        time.sleep(2)  # 假设该任务耗时2s
        print('IO任务1已完成,耗时2s')
    def taskIO_2():
        print('开始运行IO任务2...')
        time.sleep(3)  # 假设该任务耗时3s
        print('IO任务2已完成,耗时3s')
    
    start = time.time()
    taskIO_1()
    taskIO_2()
    print('所有IO任务总耗时%.5f秒' % float(time.time()-start))
    # 输出结果
    开始运行IO任务1...
    IO任务1已完成,耗时2s
    开始运行IO任务2...
    IO任务2已完成,耗时3s
    所有IO任务总耗时5.00094秒
    

    可以看到,使用同步的方式实现多个IO任务的时间是分别执行这两个IO任务时间的总和。

    下面我们使用yield fromasyncio将上面的同步代码改成异步的。修改结果如下:

    import time
    import asyncio
    
    @asyncio.coroutine # 解释1
    def taskIO_1():
        print('开始运行IO任务1...')
        yield from asyncio.sleep(2)  # 解释2
        print('IO任务1已完成,耗时2s')
        return taskIO_1.__name__
    
    @asyncio.coroutine 
    def taskIO_2():
        print('开始运行IO任务2...')
        yield from asyncio.sleep(3)  # 假设该任务耗时3s
        print('IO任务2已完成,耗时3s')
        return taskIO_2.__name__
    
    @asyncio.coroutine 
    def main(): # 调用方
        tasks = [taskIO_1(), taskIO_2()]  # 把所有任务添加到task中
        done,pending = yield from asyncio.wait(tasks) # 子生成器
        for r in done: # done和pending都是一个任务,所以返回结果需要逐个调用result()
            print('协程无序返回值:'+r.result())
    
    if __name__ == '__main__':
        start = time.time()
        loop = asyncio.get_event_loop() # 创建一个事件循环对象loop
        try:
            loop.run_until_complete(main()) # 完成事件循环,直到最后一个任务结束
        finally:
            loop.close() # 结束事件循环
        print('所有IO任务总耗时%.5f秒' % float(time.time()-start))
        
    # 输出结果
    开始运行IO任务2...
    开始运行IO任务1...
    IO任务1已完成,耗时2s
    IO任务2已完成,耗时3s
    协程无序返回值:taskIO_1
    协程无序返回值:taskIO_2
    所有IO任务总耗时3.00303秒
    

    说明

    解释1@asyncio.coroutine装饰器是协程函数的标志,我们需要在每一个任务函数前加这个装饰器,并在函数中使用yield from

    解释2:此处假设该任务运行需要2秒,此处使用异步等待2秒asyncio.sleep(2),而非同步等待time.sleep(2)

    执行过程

    1. 先通过get_event_loop()获取了一个标准事件循环loop(因为是一个,所以协程是单线程)
    2. 然后,我们通过run_until_complete(main())来运行协程(此处把调用方协程main()作为参数,调用方负责调用其他委托生成器),run_until_complete的特点就像该函数的名字,直到循环事件的所有事件都处理完才能完整结束.
    3. 进入调用方协程,我们把多个任务[taskIO_1()和taskIO_2()]放到一个task列表中,可理解为打包任务。
    4. 我们使用asyncio.wait(tasks)来获取一个awaitable objects即可等待对象的集合,通过yield from返回一个包含(done, pending)的元组,done表示已完成的任务列表,pending表示未完成的任务列表。
    5. 因为done里面有我们需要的返回结果,但它目前还是个任务列表,所以要取出返回的结果值,我们遍历它并逐个调用result()取出结果即可。
    6. 最后我们通过loop.close()关闭事件循环。

    可见,通过使用协程,极大提高了多任务的执行效率,程序最后消耗的时间是任务队列中耗时最多时间任务的时长。

    总结

    本篇讲述了:

    • yield from如何实现协程
    • 如何结合asyncio实现异步协程

    虽然有了yield from的存在,让协程实现比之前容易了,但是这种异步协程的实现方式,并不是很pythonic。现在已经不推荐使用了。下篇将与您分享更加完善的Python异步实现方式——async/await实现异步协程

    参考

    Python异步IO之协程(一):从yield from到async的使用

    关注公众号西加加先生一起玩转Python
    在这里插入图片描述

  • 相关阅读:
    软件工程——第一章 软件和软件工程的基本概念
    软件工程——第三章 软件需求分析
    软件工程——第六章 软件测试
    软件工程——第四章 面向过程的软件设计方法
    Statement和PreparedStatement之间的区别(转)
    Eclipse环境变量配置、插件安装、常见错误
    Flex动态读取XML文件并显示在DataGrid中
    修改图层的symbol(AE+C#)
    如何用Httpservice和Webservice来和Flex进行通讯(转)
    flex事件讲解(转)
  • 原文地址:https://www.cnblogs.com/ghostlee/p/12185206.html
Copyright © 2020-2023  润新知